commit 2ba7ceee7a198d32d9e0b87058cac63363aae1da Author: Andy Kopra Date: Sat Jul 18 18:48:23 2026 +0200 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) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..384fdae --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# Build artifacts +*.o +*.d +*.so + +# Editor / OS cruft +*~ +.DS_Store diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..29b6971 --- /dev/null +++ b/LICENSE.md @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..d1a8e89 --- /dev/null +++ b/README.md @@ -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). diff --git a/bin/.gitignore b/bin/.gitignore new file mode 100644 index 0000000..7c9d611 --- /dev/null +++ b/bin/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!README.md diff --git a/bin/README.md b/bin/README.md new file mode 100644 index 0000000..83f0674 --- /dev/null +++ b/bin/README.md @@ -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. diff --git a/com/.gitignore b/com/.gitignore new file mode 100644 index 0000000..66968f9 --- /dev/null +++ b/com/.gitignore @@ -0,0 +1,4 @@ +ktext +kdesc +kdiag +*.d diff --git a/com/Makefile b/com/Makefile new file mode 100644 index 0000000..9f62051 --- /dev/null +++ b/com/Makefile @@ -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) diff --git a/com/kdesc.cpp b/com/kdesc.cpp new file mode 100644 index 0000000..e6b608e --- /dev/null +++ b/com/kdesc.cpp @@ -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(); + } +} diff --git a/com/kdiag.cpp b/com/kdiag.cpp new file mode 100644 index 0000000..6c87a83 --- /dev/null +++ b/com/kdiag.cpp @@ -0,0 +1,118 @@ +#include +#include +#include + +#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 @"); + args.flag("eval", "Process eval: @eval @"); + args.flag("cond", "Process cond: @cond | | @"); + 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: @@ ... @@"); + 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::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; +} + diff --git a/com/ktext.cpp b/com/ktext.cpp new file mode 100644 index 0000000..91c9019 --- /dev/null +++ b/com/ktext.cpp @@ -0,0 +1,109 @@ +#include +#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 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; +} diff --git a/doc/edit/emacs/README.md b/doc/edit/emacs/README.md new file mode 100644 index 0000000..ea49c39 --- /dev/null +++ b/doc/edit/emacs/README.md @@ -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. diff --git a/doc/edit/emacs/klammertext-mode.el b/doc/edit/emacs/klammertext-mode.el new file mode 100644 index 0000000..528996f --- /dev/null +++ b/doc/edit/emacs/klammertext-mode.el @@ -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 diff --git a/doc/edit/sublime/Breakers.sublime-color-scheme b/doc/edit/sublime/Breakers.sublime-color-scheme new file mode 100644 index 0000000..f1d27a3 --- /dev/null +++ b/doc/edit/sublime/Breakers.sublime-color-scheme @@ -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": "" + } + ] +} diff --git a/doc/edit/sublime/Celeste.sublime-color-scheme b/doc/edit/sublime/Celeste.sublime-color-scheme new file mode 100644 index 0000000..f891976 --- /dev/null +++ b/doc/edit/sublime/Celeste.sublime-color-scheme @@ -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": "" + } + ] +} diff --git a/doc/edit/sublime/Comments.tmPreferences b/doc/edit/sublime/Comments.tmPreferences new file mode 100644 index 0000000..2aed84b --- /dev/null +++ b/doc/edit/sublime/Comments.tmPreferences @@ -0,0 +1,44 @@ + + + + + + name + Comments + scope + text.klammertext + settings + + shellVariables + + + name + TM_COMMENT_START + value + # + + + name + TM_COMMENT_START_2 + value + #[ + + + name + TM_COMMENT_END_2 + value + ]# + + + + + diff --git a/doc/edit/sublime/Default.sublime-keymap b/doc/edit/sublime/Default.sublime-keymap new file mode 100644 index 0000000..dc39952 --- /dev/null +++ b/doc/edit/sublime/Default.sublime-keymap @@ -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" } + ] + } +] diff --git a/doc/edit/sublime/Klammertext.py b/doc/edit/sublime/Klammertext.py new file mode 100644 index 0000000..42d6939 --- /dev/null +++ b/doc/edit/sublime/Klammertext.py @@ -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) diff --git a/doc/edit/sublime/Klammertext.sublime-syntax b/doc/edit/sublime/Klammertext.sublime-syntax new file mode 100644 index 0000000..e3f66fd --- /dev/null +++ b/doc/edit/sublime/Klammertext.sublime-syntax @@ -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 .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 diff --git a/doc/edit/sublime/Klammertext_in_Sublime_Text.md b/doc/edit/sublime/Klammertext_in_Sublime_Text.md new file mode 100644 index 0000000..5b12da3 --- /dev/null +++ b/doc/edit/sublime/Klammertext_in_Sublime_Text.md @@ -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 `.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. diff --git a/doc/edit/sublime/Mariana.sublime-color-scheme b/doc/edit/sublime/Mariana.sublime-color-scheme new file mode 100644 index 0000000..1f4d739 --- /dev/null +++ b/doc/edit/sublime/Mariana.sublime-color-scheme @@ -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": "" + } + ] +} diff --git a/doc/edit/sublime/Monokai.sublime-color-scheme b/doc/edit/sublime/Monokai.sublime-color-scheme new file mode 100644 index 0000000..218dc0f --- /dev/null +++ b/doc/edit/sublime/Monokai.sublime-color-scheme @@ -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": "" + } + ] +} diff --git a/doc/edit/sublime/Sixteen.sublime-color-scheme b/doc/edit/sublime/Sixteen.sublime-color-scheme new file mode 100644 index 0000000..df9ac89 --- /dev/null +++ b/doc/edit/sublime/Sixteen.sublime-color-scheme @@ -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": "" + } + ] +} diff --git a/doc/edit/sublime/example.kt b/doc/edit/sublime/example.kt new file mode 100644 index 0000000..4c2c78a --- /dev/null +++ b/doc/edit/sublime/example.kt @@ -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 diff --git a/doc/install/klammertext.zsh b/doc/install/klammertext.zsh new file mode 100644 index 0000000..ad47cf6 --- /dev/null +++ b/doc/install/klammertext.zsh @@ -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" +} diff --git a/doc/install/linux_container_install.md b/doc/install/linux_container_install.md new file mode 100644 index 0000000..32447c7 --- /dev/null +++ b/doc/install/linux_container_install.md @@ -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 +``` diff --git a/doc/install/linux_source_install.md b/doc/install/linux_source_install.md new file mode 100644 index 0000000..e0798a5 --- /dev/null +++ b/doc/install/linux_source_install.md @@ -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//bin/` 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/` — 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. diff --git a/doc/install/macos_container_install.md b/doc/install/macos_container_install.md new file mode 100644 index 0000000..9a3b65d --- /dev/null +++ b/doc/install/macos_container_install.md @@ -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 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 + ``` diff --git a/doc/install/macos_source_install.md b/doc/install/macos_source_install.md new file mode 100644 index 0000000..c0da126 --- /dev/null +++ b/doc/install/macos_source_install.md @@ -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. 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/` — 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. diff --git a/doc/install/texlive_additional_packages.sh b/doc/install/texlive_additional_packages.sh new file mode 100644 index 0000000..6592b29 --- /dev/null +++ b/doc/install/texlive_additional_packages.sh @@ -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 [mirror] +# +# 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 [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 </dev/null || echo unknown)" +BUILT="$(date -u '+%Y-%m-%d %H:%M:%S UTC')" +cat > "$TEXDIR/KLAMMERTEXT_BUILD_INFO.txt" < $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//tlnet-final +INFO + +echo "Klammertext TeX Live tree built in $TEXDIR (binaries in bin/$ARCH)" +echo "Provenance written to $TEXDIR/KLAMMERTEXT_BUILD_INFO.txt" diff --git a/lib/.gitignore b/lib/.gitignore new file mode 100644 index 0000000..7c9d611 --- /dev/null +++ b/lib/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!README.md diff --git a/lib/README.md b/lib/README.md new file mode 100644 index 0000000..51c4883 --- /dev/null +++ b/lib/README.md @@ -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. diff --git a/mac/.gitignore b/mac/.gitignore new file mode 100644 index 0000000..a438335 --- /dev/null +++ b/mac/.gitignore @@ -0,0 +1 @@ +*.d diff --git a/mac/Makefile b/mac/Makefile new file mode 100644 index 0000000..9fde99c --- /dev/null +++ b/mac/Makefile @@ -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) diff --git a/mac/argtype.cpp b/mac/argtype.cpp new file mode 100644 index 0000000..bd5c621 --- /dev/null +++ b/mac/argtype.cpp @@ -0,0 +1,84 @@ +#include +#include +#include + +#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 true_values { "1", "true", "True", "yes" }; + std::set 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 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(); +} + diff --git a/mac/argtype.h b/mac/argtype.h new file mode 100644 index 0000000..c7d1bb8 --- /dev/null +++ b/mac/argtype.h @@ -0,0 +1,51 @@ +#pragma once + +#include +#include +#include +#include + +#include "locator.h" + +using argtype_t = std::variant>; + +using modify_string_f = std::function)>; + +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 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& value); +std::string pyformat_bool(const std::vector& value); +std::string pyformat_number(const std::vector& value); +std::string pyformat_list(const std::vector& value); +std::string pyformat_dlist(const std::vector& value); diff --git a/mac/argtype_set.cpp b/mac/argtype_set.cpp new file mode 100644 index 0000000..f35aa62 --- /dev/null +++ b/mac/argtype_set.cpp @@ -0,0 +1,155 @@ +#include +#include +#include +#include + +#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::iterator begin, std::vector::iterator end, std::vector& 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(); +} diff --git a/mac/argtype_set.h b/mac/argtype_set.h new file mode 100644 index 0000000..6cc6804 --- /dev/null +++ b/mac/argtype_set.h @@ -0,0 +1,88 @@ +#pragma once + +#include +#include +#include + +#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::iterator begin, std::vector::iterator end, std::vector& 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 m_names {}; + std::map m_types {}; + size_t m_name_size = 0; + size_t m_pattern_size = 0; +}; + +const +std::string default_argtype = "string"; + +const +std::vector> 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 }, +}; diff --git a/mac/argument.cpp b/mac/argument.cpp new file mode 100644 index 0000000..6d964ed --- /dev/null +++ b/mac/argument.cpp @@ -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) +{ +} diff --git a/mac/argument.h b/mac/argument.h new file mode 100644 index 0000000..d02885e --- /dev/null +++ b/mac/argument.h @@ -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; diff --git a/mac/argument_set.cpp b/mac/argument_set.cpp new file mode 100644 index 0000000..b37ca06 --- /dev/null +++ b/mac/argument_set.cpp @@ -0,0 +1,420 @@ +#include +#include +#include + +#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& katoms) + : m_katoms(katoms) +{ + (void)K::log(3); + Argtype_set argtypes {}; + parse_parameters(m_katoms, argtypes); +} + +Parameter_set::Parameter_set(const std::vector& 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> +function_symbol_parts(katom_list::const_iterator kbegin, katom_list::const_iterator kend) +{ + std::vector> 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 +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> positional, + std::vector> optional, + std::vector 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 +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 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 +Parameter_set::check_optional(const katom_lists& optional_arguments, const Locator& loc) +{ + (void)K::log(3, optional_arguments.size()); + std::vector optional_names_used {}; + std::map 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 +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 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& 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 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; +} diff --git a/mac/argument_set.h b/mac/argument_set.h new file mode 100644 index 0000000..cabb957 --- /dev/null +++ b/mac/argument_set.h @@ -0,0 +1,63 @@ +#pragma once + +#include +#include +#include +#include "katom.h" +#include "argument.h" +#include "locator.h" + +class Argtype_set; + +std::tuple>,std::vector>,std::vector> +argument_split(std::vector::const_iterator kbegin, std::vector::const_iterator kend, + long unsigned int positional_limit = std::numeric_limits::max()); + +class Parameter_set +{ +public: + Parameter_set() {}; + ~Parameter_set() = default; + Parameter_set(const std::string parameter_string); + Parameter_set(const std::vector& katoms); + Parameter_set(const std::vector& katoms, const Argtype_set& argtypes); + + void parse_parameters(const std::vector& katoms, const Argtype_set& argtypes); + void describe_parameters(); + + void check_positional( + const std::vector>& positional_arguments, const Locator& loc); + std::map check_optional( + const std::vector>& optional_arguments, const Locator& loc); + const std::map value_map( + const std::vector>& positional, + const std::vector>& optional, + const std::vector& rest, + const Locator& loc); + bool empty() const { return m_katoms.size() == 0; }; + + std::vector m_katoms {}; + //Argtype_set m_argtypes {}; + std::vector m_positional {}; + std::vector m_optional {}; + std::vector m_optional_names {}; + std::vector m_rest {}; + size_t m_positional_count = std::numeric_limits::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> positional, + std::vector> optional, + std::vector rest); + +std::string replace_arguments( + const std::map& values, + const std::string& parameterized_text, const Locator& loc); diff --git a/mac/argv.cpp b/mac/argv.cpp new file mode 100644 index 0000000..a2ec46d --- /dev/null +++ b/mac/argv.cpp @@ -0,0 +1,569 @@ +#include + +#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 << "[]\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 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 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 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 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 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 +Argv::classify_arguments(int argc, char* argv[], bool full_parse) +{ + (void)K::log(2, argc); + if (argc == 1) { + return {}; + } + std::map named_args {}; + std::vector 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& 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 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 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 = ""; + } + std::stringstream ss {}; + ss << " "<< std::setfill(' ') << std::setw(width) + << m_args[name].symbol() << " : " << value; + } + /* + if (verbose_level == 1) { + std::cout << "\n"; + } + */ +} diff --git a/mac/argv.h b/mac/argv.h new file mode 100644 index 0000000..cf21d53 --- /dev/null +++ b/mac/argv.h @@ -0,0 +1,103 @@ +#pragma once + +// Delusions of generality, but it's really just for Klammertext commands. + +#include +#include +#include +#include + +inline std::map 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 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& words); + void parse_flags(std::vector& words, std::map& named_args); + void parse_optional(std::vector& words, std::map& named_args); + + void parse_positional( + std::string command, // std::vector words, + std::string pos_args, std::map& named_args); + + std::map classify_arguments(int argc, char* argv[], bool full_parse=true); + void check_required( + const std::vector& req_args, const std::string& command_name); + void check_flags(const std::map& 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 as_vector(const std::string& name); + std::pair> 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 m_args {}; + std::vector m_names {}; + std::vector m_req_names {}; + std::vector m_flag_names {}; + std::vector m_opt_names {}; + std::vector m_hyphen_markers {}; + long unsigned int m_syntax_size = 0; +}; diff --git a/mac/basenames.mk b/mac/basenames.mk new file mode 100644 index 0000000..a6347df --- /dev/null +++ b/mac/basenames.mk @@ -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 diff --git a/mac/character.cpp b/mac/character.cpp new file mode 100644 index 0000000..23bea89 --- /dev/null +++ b/mac/character.cpp @@ -0,0 +1,304 @@ +#include + +#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(); +} diff --git a/mac/character.h b/mac/character.h new file mode 100644 index 0000000..844cbf0 --- /dev/null +++ b/mac/character.h @@ -0,0 +1,105 @@ +#pragma once + +// https://jakubmarian.com/special-characters-diacritics-used-in-european-languages/ + +#include +#include +#include +#include +#include +#include +#include +#include + +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 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> 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 extended_latin_symbols = { + "s", + "i", "I", "t", "T", "e", "E", "o", "O", "d", "D", + "ae", "AE", "oe", "OE" +}; + +inline +std::map> 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> 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(); diff --git a/mac/command.cpp b/mac/command.cpp new file mode 100644 index 0000000..db350a4 --- /dev/null +++ b/mac/command.cpp @@ -0,0 +1,109 @@ +#include + +#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 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 +parse_args( + const std::vector& 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> 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}; +} diff --git a/mac/command.h b/mac/command.h new file mode 100644 index 0000000..986c44b --- /dev/null +++ b/mac/command.h @@ -0,0 +1,17 @@ +#pragma once + +#include +//#include +#include + +#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 +parse_args( + const std::vector& input_filenames, std::string target, + std::string output_basename, bool display_only); diff --git a/mac/deftype.cpp b/mac/deftype.cpp new file mode 100644 index 0000000..0e37757 --- /dev/null +++ b/mac/deftype.cpp @@ -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, 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; +} diff --git a/mac/deftype.h b/mac/deftype.h new file mode 100644 index 0000000..4a08d9a --- /dev/null +++ b/mac/deftype.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include + +#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); diff --git a/mac/env/lsan.supp b/mac/env/lsan.supp new file mode 100644 index 0000000..8af0970 --- /dev/null +++ b/mac/env/lsan.supp @@ -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:" line suppresses any leak whose stack trace has +# a frame matching the substring (in a function name or library path). + +leak:libpython +leak:OpenImageIO diff --git a/mac/env/makefile.env b/mac/env/makefile.env new file mode 100644 index 0000000..82b6ba3 --- /dev/null +++ b/mac/env/makefile.env @@ -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)) diff --git a/mac/env/makefile.env.hollis.DISABLED b/mac/env/makefile.env.hollis.DISABLED new file mode 100644 index 0000000..dcbd508 --- /dev/null +++ b/mac/env/makefile.env.hollis.DISABLED @@ -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 diff --git a/mac/env/makefile.env.jatke.DISABLED b/mac/env/makefile.env.jatke.DISABLED new file mode 100644 index 0000000..76e27db --- /dev/null +++ b/mac/env/makefile.env.jatke.DISABLED @@ -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 diff --git a/mac/env/makefile.env.pop.DISABLED b/mac/env/makefile.env.pop.DISABLED new file mode 100644 index 0000000..76e27db --- /dev/null +++ b/mac/env/makefile.env.pop.DISABLED @@ -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 diff --git a/mac/env/makefile.env.ubuntu.DISABLED b/mac/env/makefile.env.ubuntu.DISABLED new file mode 100644 index 0000000..76e27db --- /dev/null +++ b/mac/env/makefile.env.ubuntu.DISABLED @@ -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 diff --git a/mac/env/optimize.env b/mac/env/optimize.env new file mode 100644 index 0000000..e5ddf1e --- /dev/null +++ b/mac/env/optimize.env @@ -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 diff --git a/mac/env/runtime.env b/mac/env/runtime.env new file mode 100644 index 0000000..74a4038 --- /dev/null +++ b/mac/env/runtime.env @@ -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. file and no HOST/OS/SITE selector. +# - KLAMMERTEXT_HOME : derived from this file's own location (self-locating) +# - KLAMMERTEXT_TEXLIVE_BIN : newest ~/external/texlive//bin/ +# 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 diff --git a/mac/error.cpp b/mac/error.cpp new file mode 100644 index 0000000..dc16725 --- /dev/null +++ b/mac/error.cpp @@ -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; +} diff --git a/mac/error.h b/mac/error.h new file mode 100644 index 0000000..8b5147b --- /dev/null +++ b/mac/error.h @@ -0,0 +1,77 @@ +#pragma once + +#include + +#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) {}; +}; diff --git a/mac/eval.cpp b/mac/eval.cpp new file mode 100644 index 0000000..f53736f --- /dev/null +++ b/mac/eval.cpp @@ -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 + +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 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 @\n" + << "or\n" + << " @eval :cpp @\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; +} + diff --git a/mac/eval.h b/mac/eval.h new file mode 100644 index 0000000..c762809 --- /dev/null +++ b/mac/eval.h @@ -0,0 +1,30 @@ +#pragma once + +#include + +#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 eval( + std::vector::iterator begin, std::vector::iterator end); + + Machine m_machine; + Locator m_loc; +}; diff --git a/mac/eval_cpp.cpp b/mac/eval_cpp.cpp new file mode 100644 index 0000000..ee20c26 --- /dev/null +++ b/mac/eval_cpp.cpp @@ -0,0 +1,43 @@ +#include +#include + +#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; +} + diff --git a/mac/eval_cpp.h b/mac/eval_cpp.h new file mode 100644 index 0000000..76943e1 --- /dev/null +++ b/mac/eval_cpp.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +#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; +}; diff --git a/mac/eval_python.cpp b/mac/eval_python.cpp new file mode 100644 index 0000000..980ccc8 --- /dev/null +++ b/mac/eval_python.cpp @@ -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 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; +} diff --git a/mac/eval_python.h b/mac/eval_python.h new file mode 100644 index 0000000..292cd32 --- /dev/null +++ b/mac/eval_python.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +#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 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& katoms, + const std::vector::iterator& begin, const std::vector::iterator& end); + + Machine m_machine; + Locator m_loc; + PyObject* m_globals; + PyObject* m_locals; +}; diff --git a/mac/file.cpp b/mac/file.cpp new file mode 100644 index 0000000..9dca48d --- /dev/null +++ b/mac/file.cpp @@ -0,0 +1,571 @@ +#include +#include +#include + +#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 +find_file_recursive(const fs::path& root, const std::string& filename, bool only_one) //, Locator loc) +{ + std::vector 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 +find_file_from_roots(const std::vector& roots, const std::string& filename, bool only_one) +{ + (void)K::log(3, filename); + std::vector result {}; + for (std::string root : roots) { + std::vector 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( + 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 pathnames_with_extension( + const fs::path& dir, const std::string extension) +{ + std::vector 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 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 find_files_with_extension( +strings_t find_files_with_extension( + const fs::path& root, const std::string& ext, bool case_insensitive) +{ + //std::vector 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 filenames, + std::string prolog, std::string epilog, + std::function 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 buf1(buffer_size); + std::vector 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(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); + } diff --git a/mac/file.h b/mac/file.h new file mode 100644 index 0000000..a4c8ec4 --- /dev/null +++ b/mac/file.h @@ -0,0 +1,66 @@ +#pragma once + +#include +#include +#include +#include + +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 get_files_in_directory(const std::string& dir); +std::string find_file(const std::string& basename, std::vector search_path, + bool error_if_not_found=true); + +std::vector +find_file_recursive(const fs::path& root, const std::string& filename, + bool only_one=true); //, Locator loc=Locator()); +std::vector +find_file_from_roots(const std::vector& 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 sks_dirs(); +std::vector 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 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 find_files_with_extension( + const fs::path& root, const std::string& ext, bool case_insensitive=false); +std::string combine_files( + std::vector filenames, + std::string prolog="", std::string epilog="", + std::function 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 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()); diff --git a/mac/headers_only.mk b/mac/headers_only.mk new file mode 100644 index 0000000..33f2bf5 --- /dev/null +++ b/mac/headers_only.mk @@ -0,0 +1 @@ +HEADERS_ONLY := alias env diff --git a/mac/katom.cpp b/mac/katom.cpp new file mode 100644 index 0000000..b7bb86b --- /dev/null +++ b/mac/katom.cpp @@ -0,0 +1,397 @@ +#include +#include +#include +#include + +#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(s, Locator(), katom_t::word) }; + //return std::vector{ std::make_shared(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 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::const_iterator begin, std::vector::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 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 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 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 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 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{}; + 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}); +} diff --git a/mac/katom.h b/mac/katom.h new file mode 100644 index 0000000..7a2ec11 --- /dev/null +++ b/mac/katom.h @@ -0,0 +1,205 @@ +#pragma once + +#include +#include + +#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& katoms); +int active_count(const std::vector& katoms); +std::vector split_into_katoms(std::string s, const std::string& source, int source_line); +void restore_initial_type(std::vector::iterator begin, std::vector::iterator end); +void modify_type(katom_t new_type, std::vector::iterator begin, std::vector::iterator end); +void modify_type(katom_t old_type, katom_t new_type, + std::vector::iterator begin, std::vector::iterator end); +void ignore_whitespace(std::vector::iterator& begin, std::vector& katoms); +std::vector::iterator after_whitespace(std::vector::iterator begin); +std::vector text_katoms( + std::vector::iterator& begin, std::vector::iterator& end); + +std::string as_string(std::vector::const_iterator begin, std::vector::const_iterator end, bool strip_whitespace); + +std::string as_string(const std::vector& katoms, bool strip_whitespace); + + +std::vector trim(std::vector& katoms, std::set trim_types = {katom_t::space, katom_t::newline}); +std::vector trim(const std::vector& katoms, bool trim_inactive = false); + + +std::vector> bar_split(std::vector::iterator kbegin, std::vector::iterator kend); + +std::vector line_split(std::string s); +std::pair> line_split(fs::path pathname); +std::vector katomize(const std::vector& lines, const std::string& source_desc); + +void process_whitespace_modifiers(std::vector& katoms); +std::vector trim_whitespace(std::vector 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 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; +} diff --git a/mac/katom_list.cpp b/mac/katom_list.cpp new file mode 100644 index 0000000..3e58efe --- /dev/null +++ b/mac/katom_list.cpp @@ -0,0 +1,428 @@ +// #include +#include + +#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::const_iterator begin, std::vector::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 +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 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 +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& 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 level_inc, + std::function 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 level_inc, + std::function 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> 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 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 | | | @"; + 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()); + } + } + } +} +*/ diff --git a/mac/katom_list.h b/mac/katom_list.h new file mode 100644 index 0000000..b8fd296 --- /dev/null +++ b/mac/katom_list.h @@ -0,0 +1,52 @@ +#pragma once + +#include "katom.h" +#include "state.h" + + +std::string to_string(std::vector::const_iterator begin, std::vector::const_iterator end, bool trim_result=false); +std::string to_string(const std::vector& katoms, bool trim_result=false); + +// std::vector 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::iterator +find_katom(const std::vector::iterator begin, const std::vector::iterator end, size_t index); + +std::vector> +find_spans(std::vector::iterator begin, std::vector::iterator end, + std::function level_inc, + std::function level_dec, + bool error_exit, + std::string name); + +std::vector> +find_spans(std::vector& katoms, + std::function level_inc, + std::function level_dec, + bool error_exit=true, + std::string name="all"); + + +void describe_spans(const std::vector& katoms); + +std::pair::iterator, std::vector::iterator> +find_span_katoms(std::vector& katoms, const Katom& begin, const Katom& end); + +std::pair::iterator, std::vector::iterator> +find_span_katoms( + std::vector::iterator kbegin, std::vector::iterator kend, const Katom& begin, const Katom& end); + +void encode_nonascii_characters(std::vector& katoms); +void mark_literal_katoms(std::vector& katoms); +void mark_ignored_katoms(std::vector& katoms); + +void process_klammer_katoms(std::vector& katoms); diff --git a/mac/klammer.cpp b/mac/klammer.cpp new file mode 100644 index 0000000..136efa3 --- /dev/null +++ b/mac/klammer.cpp @@ -0,0 +1,493 @@ +#include + +#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 +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 \"\" for general klammers or \".\" " + "for a specialized target. The klammer defined as \".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 +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" + " @@[.] : @@ definition\n" + " @@[.] :: @@ instance (uses .k parameters)\n" + " @@[.] ::: @@ override existing definition\n" + " @@[.] :::: @@ 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 target_names) +{ + std::vector 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 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 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 defs {}; + std::vector target_names = targets.applicable(); + std::vector 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(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 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 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; +} + + diff --git a/mac/klammer.h b/mac/klammer.h new file mode 100644 index 0000000..81e4caf --- /dev/null +++ b/mac/klammer.h @@ -0,0 +1,106 @@ +#pragma once + +#include + +#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>; + using target_variable_map_t = std::map; + static std::regex name_re; // = std::regex(R"((\w+)(?:\.(\w+))?)"); + + struct components { + std::string target; + katom_t deftype; + Parameter_set parameters; + std::vector body; + variable_map_t varmap; + Locator loc; + }; + + // target-name -> [variable -> index] + + void add_target_definition( + std::string target_name, Argtype_set argtypes, + std::vector::iterator begin, std::vector::iterator end); + void remove_target_definition(const std::string& target_name); + + auto target_defs(std::vector 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 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 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 m_defs {}; + + // After rationalization: + Parameter_set m_parameters {}; + std::map> m_body {}; + std::string m_desc {}; + std::map m_defloc {}; // target -> Locator + std::map 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 +parse_name(Target_set targets, Katom name_katom); + +std::tuple, Locator> +parse_definition_katoms(Argtype_set argtypes, //Target_set targets, + std::vector::iterator& begin, std::vector::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); +*/ diff --git a/mac/klammer_set.cpp b/mac/klammer_set.cpp new file mode 100644 index 0000000..0e7c51f --- /dev/null +++ b/mac/klammer_set.cpp @@ -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* 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 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 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; +} diff --git a/mac/klammer_set.h b/mac/klammer_set.h new file mode 100644 index 0000000..5b6f829 --- /dev/null +++ b/mac/klammer_set.h @@ -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::iterator begin, std::vector::iterator end, std::vector& katoms); + void rationalize(Target_set targets); + void check_klammer(std::string name, std::string target, Locator loc); + const std::vector* constant_body(const std::string& name) const; + std::string instance_list(int margin) const; + std::string describe(int margin=0) const; + + std::map m_klammers {}; +}; diff --git a/mac/ktype.cpp b/mac/ktype.cpp new file mode 100644 index 0000000..de4b2f3 --- /dev/null +++ b/mac/ktype.cpp @@ -0,0 +1,207 @@ +#include +#include +#include +#include + +#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 = ""; + } else if (t.m_type == katom_t::newline) { + pat = "\\n"; + } else if (t.m_type == katom_t::ws_space) { + pat = "#+ or #+"; + } else if (t.m_type == katom_t::ws_newline) { + pat = "#/ or #/"; + } else if (t.m_type == katom_t::special) { + pat = "^@, ^|, ^#, ^:, or ^^"; + } else if (t.m_type == katom_t::apply_end) { + pat = "@ or @"; + } else if (t.m_type == katom_t::define_end) { + pat = "@@ or @@"; + } else if (t.m_type == katom_t::machine_end) { + pat = "@@@ or @@@"; + } else if (t.m_type == katom_t::ws_added) { + pat = " or \\n"; + } else if (t.m_type == katom_t::word) { + pat = ""; + } else if (t.m_type == katom_t::text) { + pat = ""; + } else if (t.m_type == katom_t::nonascii) { + pat = "^ or ^^"; + } else { + pat = t.m_pattern; + pat = string_replace(pat, definition_name, ""); + pat = string_replace(pat, "\\", ""); + } + return pat; +} + +std::string regex_display(Ktype t) +{ + std::string rgx{}; + if (t.m_type == katom_t::space) { + rgx = ""; + } 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, "" is a word that begins with a letter + and only contains letters, numbers, or the underscore (_) or period (.) characters. )"; + } + ss << "A is an integer greater than or equal to 1. " + "A 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(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; +} diff --git a/mac/ktype.h b/mac/ktype.h new file mode 100644 index 0000000..8dcc135 --- /dev/null +++ b/mac/ktype.h @@ -0,0 +1,305 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +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_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_type_names {}; +inline std::map katom_type_descs {}; +inline std::vector 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 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 spaces (default: 1)"), + Ktype(katom_t::ws_newline, "ws-newline", ws_newline_s, "Remove all whitespace, leaving 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", " 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> 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 printable_katom_types { + katom_t::word, + katom_t::space, + katom_t::newline, + katom_t::literal, + katom_t::ws_added +}; + +inline const +std::set 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 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_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); diff --git a/mac/locator.cpp b/mac/locator.cpp new file mode 100644 index 0000000..1aeaed8 --- /dev/null +++ b/mac/locator.cpp @@ -0,0 +1,137 @@ +#include + +#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& 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 locators) +{ + std::map> 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 ""; +} diff --git a/mac/locator.h b/mac/locator.h new file mode 100644 index 0000000..78b8659 --- /dev/null +++ b/mac/locator.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include +// #include +#include +#include + +#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& relpath); + +Locator current_locator( + const std::source_location location = std::source_location::current()); + +std::string locator_summary(std::vector locators); + +inline +std::string showloc(Locator loc=Locator()) { + return loc.abbrev(false) + " "; +} + diff --git a/mac/log.cpp b/mac/log.cpp new file mode 100644 index 0000000..1d0a652 --- /dev/null +++ b/mac/log.cpp @@ -0,0 +1,117 @@ +#include + +#include "log.h" +#include "show.h" +#include "util.h" + +int verbose_level = 0; + +using log_arg = std::variant; + +std::ostream& operator<<(std::ostream& os, const log_arg& arg) +{ + bool quoted_string = verbose_level > 2; + if (std::holds_alternative(arg)) { + os << (std::get(arg) ? "true" : "false"); + } else if (std::holds_alternative(arg)) { + os << std::get(arg); + } else if (std::holds_alternative(arg)) { + os << std::get(arg); + } + else if (std::holds_alternative(arg)) { + std::string s = std::get(arg); + if (quoted_string) + os << "\""; + if (!quoted_string && s.empty()) { + os << ""; + } else { + os << s; + } + if (quoted_string) + os << "\""; + } else if (std::holds_alternative(arg)) { + os << std::get(arg); +// } else if (std::holds_alternative(arg)) { +// os << std::get(arg); + } else if (std::holds_alternative(arg)) { + os << std::get(arg); +// } else if (std::holds_alternative(arg)) { +// os << std::get(arg); +// } else if (std::holds_alternative(arg)) { +// os << std::get(arg); +// } else if (std::holds_alternative(arg)) { +// os << std::get(arg); + } + /* + } else if (std::holds_alternative(arg)) { + os << std::get(arg); + } else if (std::holds_alternative(arg)) { + os << std::get(arg); + } + */ + return os; +} + +void log_indent(const std::string& filename, const std::string& color) +{ + std::cout + << color + << std::setfill(' ') + << std::setw(22-static_cast(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", "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; +} diff --git a/mac/log.h b/mac/log.h new file mode 100644 index 0000000..06b7212 --- /dev/null +++ b/mac/log.h @@ -0,0 +1,63 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#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& arg); + +void display_location(int log_level, std::source_location location); + +namespace K { +template +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) << " "), ...); + std::cout << '\n'; + } + } + } +}; + +template +log(int log_level, Ts&&...) -> log; +} + +/* +void log(int level = 3, const std::source_location location = std::source_location::current()); + +template +void log(Args... args, int level = 3, const std::source_location location = std::source_location::current()); +*/ + +/* +void log(std::vector> args={}, int level=3, + const std::source_location location + = std::source_location::current()); + +void xlog(std::vector> args={}, int verbose_override=1, + const std::source_location location + = std::source_location::current()); +*/ + +void warning(const std::string& message, const Locator& loc); diff --git a/mac/machine.cpp b/mac/machine.cpp new file mode 100644 index 0000000..139a06c --- /dev/null +++ b/mac/machine.cpp @@ -0,0 +1,528 @@ +#include +#include +#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 cond_separator_bars(katom_iter begin, katom_iter end) +{ + std::vector 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 | | | @"; + 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 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 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& 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; +} diff --git a/mac/machine.h b/mac/machine.h new file mode 100644 index 0000000..0ae7335 --- /dev/null +++ b/mac/machine.h @@ -0,0 +1,128 @@ +#pragma once + +#include + +// #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>; + + 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& katoms); + void process_eval_katoms(std::vector& katoms); + void mark_literal_klammer_content(std::vector& katoms); + void escape_target_characters(const Target& target, std::vector& katoms); + + // std::vector + void process_katoms( + std::vector& 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 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& 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& 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 m_katoms {}; +}; + +/* +class Machine { +private: + std::string m_name; + int m_id; + std::vector 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; + } +}; +*/ diff --git a/mac/show.cpp b/mac/show.cpp new file mode 100644 index 0000000..ab27a16 --- /dev/null +++ b/mac/show.cpp @@ -0,0 +1,559 @@ +#include +#include + +#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 + +std::ostream& operator<<(std::ostream& os, const std::vector& ns) +{ + if (!ns.empty()) { + auto rest = std::vector(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 + +std::ostream& operator<<(std::ostream& os, const std::vector& pp) +{ + if (!pp.empty()) { + for (const fs::path& p : pp) { + os << "<" << p.string() << ">"; + } + } + return os; +} + + +// std::map + +std::ostream& operator<<(std::ostream& os, const std::map& 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& 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(k.m_initial_type)) << right_arrow; + } + os << subscript(static_cast(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& ks) +{ + for (const auto& k : ks) { + os << k; + } + return os; +} + +std::ostream& operator<<(std::ostream& os, const std::vector& 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 ks) +{ + std::ranges::copy(ks, std::ostream_iterator(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 + +std::ostream& operator<<(std::ostream& os, const std::vector& 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 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(s)) { + ss << " String: " << trim(std::get(s)) << "\n"; + } else { + ss << " Filename: " << std::get(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; +}; diff --git a/mac/show.h b/mac/show.h new file mode 100644 index 0000000..c1e01c1 --- /dev/null +++ b/mac/show.h @@ -0,0 +1,158 @@ +#pragma once + +#include +#include + +#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 abbrev(const std::vector& ks, unsigned int max_length=16); + +// std::vector + +std::ostream& operator<<(std::ostream& os, const std::vector& ns); + +// std::vector + +std::ostream& operator<<(std::ostream& os, const std::vector& ss); + +// std::vector + +std::ostream& operator<<(std::ostream& os, const std::vector& pp); + +// std::map + +std::ostream& operator<<(std::ostream& os, const std::map& sm); + +// Katom iterator pair + +std::ostream& operator<<( + std::ostream& os, + const std::pair::iterator, std::vector::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>& ks); +std::ostream& operator<<(std::ostream& os, const std::shared_ptr& k); +std::ostream& operator<<(std::ostream& os, const std::vector::iterator& k); +std::ostream& operator<<(std::ostream& os, const std::vector& ks); +std::ostream& operator<<(std::ostream& os, const std::vector::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& ks); +std::ostream& operator<<(std::ostream& os, const std::vector& 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 +std::ostream& operator<<(std::ostream& os, const std::vector& 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); diff --git a/mac/state.cpp b/mac/state.cpp new file mode 100644 index 0000000..ef6b4ed --- /dev/null +++ b/mac/state.cpp @@ -0,0 +1,308 @@ +#include +#include + +#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 Frame::names() const +{ + std::vector 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 Frame::get(std::string name) +{ + std::pair 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 ' 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 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 State::all_names() +{ + std::vector 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 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 = ""; + } + ss << margin << " " << std::setw(width) << std::left << key << " " + << abbrev(print_value) << "\n"; + } + } + } + ss << "\n"; + return ss.str(); +} diff --git a/mac/state.h b/mac/state.h new file mode 100644 index 0000000..d33150e --- /dev/null +++ b/mac/state.h @@ -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 names() const; + void set(std::string name, std::string value, + std::string delim=":", std::string desc="", Locator loc=Locator()); + std::pair get(std::string name); + + std::string m_name {}; + std::map 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 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::iterator begin, std::vector::iterator end); + + void parse_state_katoms(std::vector::iterator begin, std::vector::iterator end, katom_list katoms); + std::vector all_names(); + std::string python_code(); + std::string describe(bool show_environment=false, int margin_size=2) const; + + std::vector 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"); +}; diff --git a/mac/target.cpp b/mac/target.cpp new file mode 100644 index 0000000..ca244f7 --- /dev/null +++ b/mac/target.cpp @@ -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> +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> 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); + } +} diff --git a/mac/target.h b/mac/target.h new file mode 100644 index 0000000..c7a36e4 --- /dev/null +++ b/mac/target.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include + +#include "katom.h" +#include "locator.h" + +class Target +{ +public: + Target() = default; +// Target(std::vector::iterator begin, std::vector::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> transforms); + void transform(std::vector& 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 m_includes {}; + std::vector m_provides {}; + std::vector m_after_apply {}; + + Locator m_loc {}; + std::vector> m_transforms {}; + std::vector> m_escapes {}; +// Argtype_set m_argtypes {}; + + +}; + +std::vector> +parse_transforms(std::string transform_string); diff --git a/mac/target_set.cpp b/mac/target_set.cpp new file mode 100644 index 0000000..25b8753 --- /dev/null +++ b/mac/target_set.cpp @@ -0,0 +1,160 @@ +#include +#include +#include + +#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::iterator begin, std::vector::iterator end, std::vector& 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 Target_set::user_defined() +{ + return collect_if( + m_names, [](auto name) { + return name != Target_set::declare_name && name != Target_set::general_name; }); +} + +std::vector 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"; + } +} +*/ diff --git a/mac/target_set.h b/mac/target_set.h new file mode 100644 index 0000000..364ba9e --- /dev/null +++ b/mac/target_set.h @@ -0,0 +1,74 @@ +#pragma once +#include +#include + +#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::iterator begin, std::vector::iterator end, std::vector& 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> transforms); + */ + + + bool has(std::string target_name); + Target get(std::string target_name, Locator loc); + void transform(std::string target_name, std::vector& katoms); + + std::vector user_defined(); + std::vector applicable(); + + std::string describe(int margin=2, bool long_format=false) const; + + //Argtype_set m_argtypes {}; + std::map m_targets {}; + std::vector m_names {}; + std::vector 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 targets {}; + std::vector names {}; + void describe_suffixes(); + void describe(); + */ +}; diff --git a/mac/util.cpp b/mac/util.cpp new file mode 100644 index 0000000..5e2acc4 --- /dev/null +++ b/mac/util.cpp @@ -0,0 +1,516 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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& 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(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 map_key_lengths(std::map map) +{ + int result = 0; + for (auto const& item: map) { + result = std::max(result, item.first.size()); + } +} + + +int +std::map m; +std::vector key, value; +for(std::map::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> environment_variables(bool allow_empty_definitions) +{ + // std::cout << "read_environment:\n"; + std::vector> 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& 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 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 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 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 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 elements { + regex_split(elements_str, std::regex(delimiter), true) }; + return elements; + } +} + +std::string get_env_var(const std::string& var) { + std::lock_guard 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 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 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; +} diff --git a/mac/util.h b/mac/util.h new file mode 100644 index 0000000..00bdc9c --- /dev/null +++ b/mac/util.h @@ -0,0 +1,107 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +inline std::mutex env_mutex; + +class Katom; + +using strings_t = std::vector; +using string_pairs = std::vector>; +using string_map = std::map; + +using katom_ptr = std::shared_ptr; +using katom_list = std::vector; +using katom_lists = std::vector; +using katom_list_map = std::map; +using katom_iter = katom_list::iterator; +using spans_t = std::vector>; +using argument_value_map = std::map; + +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& strings, const std::string& element); +std::vector regex_split(std::string s, std::regex re, bool trim_parts=true); +std::vector word_split(const std::string& s); +bool is_in(std::string s, std::vector v); +bool is_not_in(std::string s, std::vector v); +std::vector find_all(std::string str, std::regex pattern, int match_group=0); +std::vector 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& 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& things); +std::string to_be(int count, bool present = true); +int max_length(std::vector ss); +std::vector> 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& ss, std::string removed); +void remove_duplicates(std::vector& ss); +std::string display_string(const std::string& s, unsigned int width=40, bool replace_newlines=true); +std::pair extract_parameter_type(std::string parameter_name); +std::tuple regex_split_prefix(const std::regex& pattern, const std::string& text); +std::vector 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 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 +std::vector collect_if(const std::vector& xs, Pred pred) { + std::vector out; + out.reserve(xs.size()); + for (const auto& v : xs) { + if (pred(v)) out.push_back(v); + } + out.shrink_to_fit(); + return out; +} + +template +bool all_equal(const std::vector& v) { + if (v.size() < 2) return true; + return std::adjacent_find(v.begin(), v.end(), std::not_equal_to{}) == v.end(); +} + +template +int max_key_length(std::map map) +{ + size_t result = 0; + for_each(map.begin(), map.end(), + [&result](const auto& item) { result = std::max(result, item.first.size()); }); + return result; +} + diff --git a/sks/Makefile b/sks/Makefile new file mode 100644 index 0000000..1538d63 --- /dev/null +++ b/sks/Makefile @@ -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 diff --git a/sks/block/block.k b/sks/block/block.k new file mode 100644 index 0000000..f5fb399 --- /dev/null +++ b/sks/block/block.k @@ -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 :: +
+*s* +
+@@ + +@@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 :: +
+*s* +
+@@ + +@@right.k s : Right-justified text @@ + +@@right.html :: +TBD *s* +@@ + +@@right.tex :: +\begin{flushright} +*s* +\end{flushright} +@@ + +@@nl.k : Newline character @@ +@@nl.html ::
@@ +@@nl.tex :: \newline @@ +@@nl.txt :: \n @@ + +@@tnl.k : Table newline (deprecated; check) @@ +@@tnl.html ::
@@ +@@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} +@@ diff --git a/sks/block/block.py b/sks/block/block.py new file mode 100644 index 0000000..40f3b6e --- /dev/null +++ b/sks/block/block.py @@ -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'''
+{self.label}: {self.s} +
''' + 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 + "
\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 diff --git a/sks/block/css/block.css b/sks/block/css/block.css new file mode 100644 index 0000000..857deea --- /dev/null +++ b/sks/block/css/block.css @@ -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; +} diff --git a/sks/block/css/list.txt b/sks/block/css/list.txt new file mode 100644 index 0000000..62d655b --- /dev/null +++ b/sks/block/css/list.txt @@ -0,0 +1 @@ +block.css diff --git a/sks/block/js/justify.js b/sks/block/js/justify.js new file mode 100644 index 0000000..51f6e41 --- /dev/null +++ b/sks/block/js/justify.js @@ -0,0 +1,30 @@ + +function adjust_footnotes() { + return; + const nodes = document.querySelectorAll(".footnote") + + document.querySelectorAll(".footnotetext"); + nodes.forEach(function (node) { + const left = node.parentElement.position().left; + const c = document.getElementById("text"); + const half_width = c.width() / 2.0; + const mid = c.position().left + half_width; + const max_width = 400; + if (left > mid) { + node.style.right = "0em"; + node.style.left = "auto"; + } else { + node.style.left = "0em"; + node.style.right = "auto"; + } + node.setAttribute("width", Math.min(max_width, half_width)); + }); +} + +/* +window.onload = function() { + // This means "no animation", not "don't show the tooltip." + console.log("tooltip: " + document.tooltip, document.getAttribute("tooltip")); + //document.tooltip.setAttribute("hide", false); + //document.tooltip.setAttribute("show", false); +}; +*/ diff --git a/sks/block/sty/block.sty b/sks/block/sty/block.sty new file mode 100644 index 0000000..7866b87 --- /dev/null +++ b/sks/block/sty/block.sty @@ -0,0 +1,20 @@ +%\usepackage{quoting} +%\quotingsetup{vskip=0pt} +\usepackage{float} +\usepackage{multicol} + +%\usepackage[absolute,showboxes]{textpos} +\usepackage[absolute]{textpos} +\setlength{\TPHorizModule}{\paperheight} +\setlength{\TPVertModule}{\paperwidth} + +%\setlength{\TPVertModule}{1\paperheight} +\textblockorigin{0in}{0in} + +\newlength{\fwboxwidth} +\newcommand{\fwboxn}[1]{\setlength{\fwboxwidth}{#1}} +\newcommand{\fwboxs}[1]{\setlength{\fwboxwidth}{\widthof{#1}}} +\newcommand{\fwbox}[1]{\makebox[\fwboxwidth][l]{#1}} + +% No space if top of page: +\newcommand{\topspace}[1]{\ifdim\pagetotal=0pt\else\vspace*{#1}\fi} diff --git a/sks/book/Makefile b/sks/book/Makefile new file mode 100644 index 0000000..f644326 --- /dev/null +++ b/sks/book/Makefile @@ -0,0 +1,17 @@ +FLAGS = -std=c++17 -fvisibility=hidden -I $(KLAMMERTEXT_HOME)/src + +ifdef OPTIMIZE +OPTIMIZE = -O3 +else ifdef NOPYTHON +OPTIMIZE = -O0 -g -DNOPYTHON +else +OPTIMIZE = -ggdb +endif + + +book.so : book.cpp + $(CXX) $(FLAGS) $(OPTIMIZE) -fPIC -c book.cpp + $(CXX) $(FLAGS) $(OPTIMIZE) -shared -o book.so book.o + +clean : + rm -f book.o book.so *~ diff --git a/sks/book/book.cpp b/sks/book/book.cpp new file mode 100644 index 0000000..f18470f --- /dev/null +++ b/sks/book/book.cpp @@ -0,0 +1,115 @@ +#include +#include +#include "util.h" +#include "eval.h" +#include "machine.h" +#include "error.h" + + //Machine M {}; + //Text T(true); + +using namespace std; + + +string single_page(Strings pages) +{ + // Combine files, surround with top-level + stringstream body {}; + for (string page : pages) { + body << page << "\n\n"; + } + stringstream html {}; + html << "\n\n" << body.str() << ""; + return html.str(); +} + +string multi_page(Strings pages) +{ + // Write chapter files and index.html; return empty chapter HTML filenames + return "MULTI_PAGE"; +} + + +string chapterbook_html( + string title, string subtitle, Strings chapters, Strings chapter_texts, bool directory_output) +{ + SHOW; + debug = xdebug = true; + Text T(true); + T.parse(); + Machine M {}; + M.state.statevars.set("_program_name", "book"); + M.state.statevars.set("_sks_enabled", "true"); + M.extract_definitions(T); + + Strings pages {}; + for (unsigned int i = 0; i < chapters.size(); i++) { + cout << "Processing chapter: " << chapters[i] << "\n"; + M.preserve_parse(T); + //cout << chapter_texts[i] << "\n"; + Strings ss { chapter_texts[i] }; + Text T(ss, {}, false); + T.parse(); + M.extract_definitions(T); + M.apply("html", T); + //cout << M.to_string("html", T, true, false); + pages.push_back(M.to_string("html", T, false, false)); + M.restore_parse(T); + } + if (directory_output) + return multi_page(pages); + else + return single_page(pages); +} + +string chapterbook_tex( + string title, string subtitle, Strings chapters, Strings chapter_texts, int leading) +{ + SHOW; + stringstream ss {}; + ss << "@document :title " << title << " :subtitle " << subtitle << " :leading " << leading << " |\n "; + for (string s : chapter_texts) + ss << s << "\n\n"; + ss << "@\n"; + //cout << ss.str() << "\n"; + return ss.str(); +} + +bool parse_output_filename(string output_filename, string target) +{ + return (extension(output_filename) != target) and (output_filename != "-"); +} + + +extern "C" VISIBLE string chapterbook(std::map args) +{ + SHOW; + show_args(args); + + string title = args["title"]; + string subtitle = args["subtitle"]; + + Strings chapters = word_split(args["chapters"]); + Strings chapter_texts {}; + for (auto c : chapters) { + string filename { "kt/" + c + ".kt" }; + cout << "Reading " << filename << "\n"; + string chapter = string_from_file(filename); + chapter = regex_split(chapter, regex("@###"))[0]; + //cout << chapter << "\n\n"; + chapter_texts.push_back(chapter); + } + string target = args["_target"]; + if (target == "html") { + bool directory_output = parse_output_filename(args["_output_filename"], target); + return chapterbook_html(title, subtitle, chapters, chapter_texts, directory_output); + } else if (target == "tex") { + int leading = stoi(args["leading"]); + return chapterbook_tex(title, subtitle, chapters, chapter_texts, leading); + } else { + stringstream msg {}; + msg << "The \"book\" klammer is not defined for target \"" << target << "\"."; + throw Definition_error(msg.str(), Locator(__FILE__, __LINE__, 0)); + } + +} diff --git a/sks/book/book.k b/sks/book/book.k new file mode 100644 index 0000000..33b0c4e --- /dev/null +++ b/sks/book/book.k @@ -0,0 +1,6 @@ +@@book.k title | chapters +:subtitle +:leading 1.1 +: Top-level book structure @@ + +@@book :: @eval book:chapterbook() eval@ @@ diff --git a/sks/code/code.k b/sks/code/code.k new file mode 100644 index 0000000..2adee4d --- /dev/null +++ b/sks/code/code.k @@ -0,0 +1,126 @@ + +@@code.k :filename :pattern :caption :number | text.literal : +A source file displayed verbatim +@@ + +@@code :: @eval code_format.Code(K) @ @@ + +@@c.k code_text : +A word or phrase displayed verbatim in a line +@@ + +@@c :: @eval code_format.Code_fragment(K) eval@ +@@ + +@@source_file filename : @eval code_format.Source(K) @ @@ + + +#[ +@@code.k text :file :number.bool true :caption : Code listing with formatted comments @@ + +@@code : +@eval code_format.Code(K) eval@ +@@ + + +@@lst spec.figure_id : + @reference *spec* | Listing @ +@@ + + + +c +code :caption :filename :pattern + + +# -------------------------------------------------------------------------------- +@@codebox.k s :color 1,1,1 :size normalsize :escapechar ^^ + :space_break_only.bool false :linenumber.bool false :scale 1.0 + :indent :standalone :vcenter : +Verbatim text for source code preserving whitepace, surrounded by a box +that extends to the margins @@ + +@@codebox.html :: + @code :text *s* @ +@@ + +@@codebox.tex :: +\definecolor{codeboxbgcolor}{rgb}{*color*} +\setlength{\codeboxlinelength}{\linewidth - 6pt} +\vspace*{4pt} +\begin{lstlisting}% +[frame=single, +framerule=1pt, +basicstyle=\*size*\ttfamily, +lineskip=0pt, +linewidth=*scale*\codeboxlinelength, +columns=fullflexible, +keepspaces=true, +framesep=6pt, +xleftmargin=6pt, +escapechar=*escapechar*, +breaklines=true, +prebreak=\hbox{\large$\mapsto$}, +%postbreak={\textbf{\hbox{$\rightarrow$}}}, +rulecolor=\color{codeboxcolor}, +backgroundcolor=\color{codeboxbgcolor}, +breakatwhitespace=*space_break_only*, +numbers=none, # #- @if *linenumber* | left | none @ #- , +numbersep=12pt, +numberstyle=\small\color{Darkred}] +*s* +\end{lstlisting} +@@ + +@@codebox.txt :: + @code :text *s* @ +@@ + +@@pathname.k s :small.bool false : Pathname @@ +@@pathname :: @eval code_format.Pathname(K) eval@ @@ + +@@annotate.k text :caption : +Comments put in boxes to the right of the code +@@ + +# @@annotate : @eval code_format.Annotate(K) eval@ @@ +# @@annotate : @codebox *text* @ @@ +@@annotate :: @code :text *text @ @@ + +@@listing s : @code :text *s* @ @@ + +# -------------------------------------------------------------------------------- +@@sv.k s : Italic font for variable in @t syntax @ argument @@ +@@sv.tex :: ^^textrm"^^textit"*s*$$ @@ +@@sv.html :: @ri *s* @ @@ + +@@svs.k s : Sans-serif font for variable in @t syntax @ argument @@ +@@svs.tex :: "^^small^^textsf"^^textit"*s*$$$ @@ +@@svs.html :: @s @i *s* @ @ @@ + +@@svsub.k base | sub : Italic font for subscripted variable in @t syntax @ argument @@ +@@svsub.tex :: ^^textrm"^^textit"*base*$$^^textsubscript"*sub*$ @@ +@@svsub.html :: *base**sub* @@ + +@@syntax.k s :fontsize normalsize :indent.bool true : +Verbatim text that includes italic font for syntax descriptions @@ + +@@syntax.html :: +@code :text *s* @ # :indent *indent* @ +@@ + +@@syntax.tex :: +\vspace*{8pt}\begin{LVerbatim}[xleftmargin=0pt, # @if true | 0pt | -24pt @ , +baselinestretch=1.05, fontsize=\*fontsize*, frame=single, framesep=8pt, +commandchars=\\̈\$, fontfamily = @verbatimfont@, framesep=12pt] +*s* +\end{LVerbatim} +@@ + +@@svspace.tex length : + ^^vspace*"*length*$ +@@ + +@@svspace.html length : +@@ +]# diff --git a/sks/code/code_format.py b/sks/code/code_format.py new file mode 100644 index 0000000..96f146b --- /dev/null +++ b/sks/code/code_format.py @@ -0,0 +1,318 @@ +if __name__ == "__main__": + import sys + sys.path.append("../kutil") + sys.path.append("../target") + +import re +import klammer_base +import kutil +import html_util +from html_util import E +import latex_util as L +import pprint +import phases + +def escape_newlines(s): + return re.sub("\n", " ___NL___ ", s) + +# def literal_newline(s): +# def replace(match): +# before, after = match.groups() +# return f"{before}\\n{after}" +# backslash_pat = re.compile(r'(".*?)\n(.*?")', re.S) +# return backslash_pat.sub(replace, s) + +def get_blocks(s): + comment_pat = re.compile(r"(\s*)//(\d+)\s+(.*)", re.S) + blocks = [] + lines = s.strip("\n").split("\n") + i = 0 + uncommented = "" + while i < len(lines): + match = comment_pat.match(lines[i]) + if match: + if uncommented: + blocks.append([uncommented.rstrip(), None]) + uncommented = "" + count = int(match.group(2)) + comment = match.group(3) + code = "" + j = 0 + i += 1 + while j < count: + line = re.sub("\n", "\\n", lines[i]) + code += line + "\n" + j += 1 + i += 1 + blocks.append([code.strip("\n"), comment]) + else: + uncommented += lines[i] + "\n" + i += 1 + if uncommented: + blocks.append([uncommented.rstrip(), None]) + return blocks + +def latex_spaces(s): + def replace(match): + s = match.group(0) + if False and len(s) == 1: + return "~" + else: + result = "~" * len(s) + result = f"\\hphantom{{{result}}}" + return result + space_pat = re.compile(" +", re.S) + return space_pat.sub(replace, s) + +def latex_unquote(s): + quoted = "asciicircum quotesingle asciigrave asciitilde asciitilde backslash".split() + quoted = [f"{{}}\text{e}{{}}" for e in quoted] + result = s + for q in quoted: + result = re.sub(q, "X", result) + result = re.sub(" ", "Y", result) + return result + +def longest_line(s): + result = "" + for line in latex_unquote(s).split("\n"): + if len(line) > len(result): + result = line + return result + +def literal_newline(s): + def replace(match): + before, after = match.groups() + return f"{before}\\n{after}" + backslash_pat = re.compile(r'(".*?)\n(.*?")', re.S) + return backslash_pat.sub(replace, s) + +class Code(klammer_base.Klammer_base): + id = 0 + def __init__(self, K): + super().__init__(K) + self.text = phases.expand_whitespace_markers(self.text) + + def html(self): + if self.K_target == "html": + self.text = literal_newline(self.text) + # Escape Klammertext special characters so they survive + # re-insertion into the katom stream after @eval + self.text = self.text.replace("^", "^^") + self.text = self.text.replace("#", "^#") + self.text = self.text.replace("@", "^@") + self.text = self.text.replace("|", "^|") + self.blocks = get_blocks(self.text) + result = '' + for text, comment in self.blocks: + border = "code_border" if comment else "code_no_border" + body = E("div").body(text).cls(f"code_text {border}").str(None) + if comment: + body += "\n" + E("div").body(comment).cls("code_comment").str() + result += E("div").body(body).cls("code_block").str() + if self.number or self.caption: + #result = html_util.add_caption( + # result, "Listing", self.number, self.caption, "i", "left", "top") + caption = kutil.caption_marker("Listing", self.caption) + result = f'
{caption}
{result}\n' + return result + + @staticmethod + def escape_latex(s): + """Escape LaTeX special characters in code text.""" + # Backslash must be first (before adding more backslashes) + s = s.replace("\\", "\\textbackslash{}") + s = s.replace("{", "\\{") + s = s.replace("}", "\\}") + s = s.replace("%", "\\%") + s = s.replace("$", "\\$") + s = s.replace("&", "\\&") + s = s.replace("_", "\\_") + s = s.replace("^", "\\textasciicircum{}") + s = s.replace("~", "\\textasciitilde{}") + s = s.replace("<", "\\textless{}") + s = s.replace(">", "\\textgreater{}") + return s + + def tex(self): + # Escape Klammertext special characters + self.text = self.text.replace("^", "^^") + self.text = self.text.replace("#", "^#") + self.text = self.text.replace("@", "^@") + self.text = self.text.replace("|", "^|") + # Escape LaTeX special characters in code text + self.text = Code.escape_latex(self.text) + self.blocks = get_blocks(self.text) + strutvis = "0pt" + start_strut = f"\\rule[0pt]{{{strutvis}}}{{12pt}}" + end_strut = f"\\rule[-6pt]{{{strutvis}}}{{12pt}}" + caption_strut = f"\\rule[-8pt]{{{strutvis}}}{{6pt}}" + indent = "8pt" + comment_sep = "10pt" + i = 0 + result = "" + count = len(self.blocks) + for text, comment in self.blocks: + text = " " + re.sub("\n", " \n ", text) + " " + longest = longest_line(text) + text = latex_spaces(text) + text = re.sub("\n", r"\\\\", text) + text = f"{start_strut}\\ttfamily {text}{end_strut}" + width = f"\\widthof{{\\ttfamily {longest}}}" + code = L.environment("minipage", text, width) + "\\\\\n" + if comment: + width = f"\\linewidth - {width} - {indent} - {comment_sep}" + code = f"\\fcolorbox{{Gray}}{{LightGray}}{{{code}}}" + code += f"\\rule{{{comment_sep}}}{{{strutvis}}}" \ + + L.environment("minipage", "\\sffamily\\small\\raggedright " + comment, width) + result += f"\\rule{{{indent}}}{{{strutvis}}}{code}" + if comment: + if i < count - 1 and self.blocks[i+1][1]: + result += "\\\\[4pt]" + i += 1 + if not self.blocks[count-1][1]: + result = result[:-4] + if self.number or self.caption: + caption = kutil.caption_marker("Listing", self.caption) + if self.blocks[0][1]: + caption += caption_strut + strut = f"\\rule{{{indent}}}{{{strutvis}}}" + result = f"{strut}\\emph{{\\it {caption}}}\\newline\n" + result + "\n" + result = f"\\hypertarget{{Reference-Listing-{Code.id}}}{{}}\n{result}" + Code.id += 1 + return result + +def undash(s): + result = s + result = re.sub("__MDASH__", "---", result) + result = re.sub("__NDASH__", "--", result) + return result + +class Code_fragment(klammer_base.Klammer_base): + def __init__(self, K): + super().__init__(K) + + def html(self): + #print(f"code: |{self.code_text}|") + result = self.code_text.strip() + result = undash(result) + #result = re.escape(result) + result = re.sub("<", "<", result) + result = re.sub(" ", " ", result) + #print(f"code: |{self.code_text}| -> |{result}|") + return f'{result}' + + def tex(self): + return f"{{\\tt {self.code_text.strip()}}}" + +def show(s): + print("-"*80) + print(s) + print("-"*80) + +class Source(klammer_base.Klammer_base): + def __init__(self, K): + super().__init__(K) + with open(self.filename) as fp: + self.src = fp.read() + + def tex(self): + result = self.src + # result = re.sub("#", "^#", result) + # result = re.sub("\\^", "\\^", result) + result = f"\\begin{{verbatim}}\n{result}\n\\end{{verbatim}}\n" + return result + + def html(self): + result = escape_newlines(self.src.strip()) + "\n" + result = re.sub("@", "^@", result) + result = E("div").body(result).cls("code_text").str() + return result + +# -------------------------------------------------------------------------------- + +if __name__ == "__main__": + s = """ +int main(int argc, char* argv[]) +{ + //1 One line commented + int count = 12; + //3 Two lines commented + for (int i = 0; i < count; i++) { + std::cout << "Counter: " << i << "\n"; + } + //1 A really long comment for one line. A really long comment for one line. A really long comment for one line. + std::cout << "End\n"; +} +""" + get_blocks(s); + + +# class Code(klammer_base.Klammer_base): +# def __init__(self, K): +# super().__init__(K) +# #self.show("Code") +# if self.filename and self.text: +# raise Exception("Both :text and :filename cannot be defined") +# if K.filename: +# with open(K.filename) as fp: +# self.src = fp.read() +# if K.pattern: +# rgx = re.compile(f".*?({K.pattern}).*", re.S) +# match = rgx.match(self.src) +# if match is None: +# raise Exception(f"Match fails for @source_code: {K.pattern}") +# self.src = match.group(1) +# self.src = kutil.protect_klammertext_special_characters(self.src) +# else: +# self.src = self.text + +# def html(self): +# #src = re.sub("\n", "", self.src) ? +# #result = f'
\n{self.src}\n
\n' +# result = self.src +# result = undash(result) +# result = f'
\n{result}\n
\n' +# return result + +# def tex(self): +# src = self.src +# src = re.sub(r"\\{", "{", src) +# src = re.sub(r"\\}", "}", src) +# result = f"\\begin{{lstlisting}}\n{src}\n\\end{{lstlisting}}\n" +# return result + +# def txt(self): +# return "x~ " + self.src + +# class Pathname(klammer_base.Klammer_base): +# def __init__(self, K): +# super().__init__(K) + +# def html(self): +# return f'{self.s}' + +# def tex(self): +# result = self.s +# def replace(match): +# return '\\{}'.format(match.group(1)) +# result = re.sub(r'\\', 'XXXBACKSLASHXXX', result) +# result = re.compile('\s*__UNSPACE__\s*', re.S).sub('', result) +# result = re.compile(r'([&${}%#_])').sub(replace, result) +# result = re.sub('\^', r'\\^{}', result) +# result = re.sub('~', r'\\~{}', result) +# result = re.sub(r'XXXBACKSLASHXXX', r'{\\textbackslash}', result) +# result = re.sub('\n', r'~\\\\\n', result.strip()) +# result = re.sub(' ', '$~$', result) +# result = re.sub("'", r"{\\textquotesingle}", result) +# result = re.sub('"', r'{\\textquotedbl}', result) +# result = re.sub('--', '{-}{-}', result) +# result = r'{{\normalfont\texttt{{{}}}}}'.format(result.strip()) +# result = re.sub(r'\{\\textbackslash\}\\#', '\\#', result) +# if self.small: +# result = '{{\\footnotesize{}}}'.format(result) +# return result + +# def txt(self): +# return f"'{self.s}'" + diff --git a/sks/code/css/code.css b/sks/code/css/code.css new file mode 100644 index 0000000..41f9c49 --- /dev/null +++ b/sks/code/css/code.css @@ -0,0 +1,57 @@ +.code { + font-family: var(--monospace); +} + +.code_block { + display: flex; + align-items: center; + margin: .125rem 0 0 1rem; + padding: 0; +/* flex-direction: column-reverse; */ +} + +.code_caption { + margin-left: 1rem; + font-style: italic; +} + +.code_text { + display: inline-block; +/* vertical-align: top; */ + white-space: pre; + font-family: var(--monospace); + line-height: 1.2; +} + +.code_comment { + display: inline-block; +/* vertical-align: top; */ + padding: .25rem; + border: solid white 1px; + padding: .25rem .25rem .25rem .5rem; + font-family: var(--sans-serif); + font-style: italic; + font-size: .8rem; + min-width: 100px; + line-height: 1.25; +} + +.code_border { + border: solid gray 1px; + /* margin: .125rem; */ + margin: .125rem .125rem .125rem .25rem; + padding: .125rem .5rem .25rem .5rem; + background-color: rgb(95%,95%,95%); + +} + +.code_no_border { + padding: .125rem 0 .125rem .5rem; + margin: 0 0 0 .125rem; + border: solid white 1px; + /* Debugging: + border: solid lightgray 1px; + background-color: rgb(250,250,127); + */ +} + diff --git a/sks/code/css/list.txt b/sks/code/css/list.txt new file mode 100644 index 0000000..735650a --- /dev/null +++ b/sks/code/css/list.txt @@ -0,0 +1 @@ +code.css diff --git a/sks/code/js/code.js b/sks/code/js/code.js new file mode 100644 index 0000000..0387945 --- /dev/null +++ b/sks/code/js/code.js @@ -0,0 +1,20 @@ + +function adjust_code_comment_width() +{ + let max_width = K.get("#text").offsetWidth; + K.getv(".code_comment").forEach(function (comment_box) { + let block = comment_box.parentNode; + let code_box = block.children[0]; + let comment_width = max_width - code_box.offsetWidth; + K.width(comment_box, comment_width); + }); +} + +window.addEventListener("load", function (event) { + window.addEventListener( + "resize", + function (event) { + adjust_code_comment_width(); + }); + adjust_code_comment_width(); +}); diff --git a/sks/code/sty/code.sty b/sks/code/sty/code.sty new file mode 100644 index 0000000..200cb29 --- /dev/null +++ b/sks/code/sty/code.sty @@ -0,0 +1,9 @@ +\usepackage{etoolbox} +\usepackage{fancyvrb} + +\usepackage{listings} +\lstset{basicstyle=\ttfamily,fontadjust=true,basewidth=0.5em,xleftmargin=26pt} + +\usepackage[strings,nohyphen]{underscore} + +\usepackage{mdframed} diff --git a/sks/color/color.k b/sks/color/color.k new file mode 100644 index 0000000..e36456c --- /dev/null +++ b/sks/color/color.k @@ -0,0 +1,8 @@ +@@@argtype color | + a color in the form . + :pattern 'float' 'float' 'float' + :python_cast (lambda s : [float(e) for e in s.strip().split()]) +@@@ + +@@color.k rgb :text : Color description as required by the target from @c ,, @ input @@ +@@color :: @eval color.Color(K) eval@ @@ diff --git a/sks/color/color.py b/sks/color/color.py new file mode 100644 index 0000000..c34f78a --- /dev/null +++ b/sks/color/color.py @@ -0,0 +1,37 @@ +import re +import klammer_base +import kutil + +class Color(klammer_base.Klammer_base): + def __init__(self, K): + kutil.msg() + super().__init__(K) + match = re.compile(r'([0-9.]+)\s*,\s*([0-9.]+)\s*,\s*([0-9.]+)', re.S).match(self.rgb) + if not match: + raise fehler.KlammertextError( + 'Error in color format: "{}". '.format(K.rgb) + + 'Should be ",,", where the components are [0,1].') + self.r, self.g, self.b = [float(c) for c in match.groups()] + + def html(self): + result = 'rgb({},{},{})'.format(*[int(round(float(e*255))) for e in [self.r, self.g, self.b]]) + #result = f"rgb({self.r},{self.g},{self.b}" + if self.text: + result = f'{self.text}' + return result + + def tex(self): + result = '\\color[rgb]{{{},{},{}}}'.format(self.r, self.g, self.b) + if self.text: + result = '{{{} {}}}'.format(result, self.text) + return result + +def hex_color(name, hex): + def c(h): + return float(eval('0x' + h)) / 255.0 + r = c(hex[:2]) + g = c(hex[2:4]) + b = c(hex[4:]) + return '\\definecolor{{{}}}{{rgb}}{{{:.3f},{:.3f},{:.3f}}}'.format( + name, r, g, b) + diff --git a/sks/color/sty/color.sty b/sks/color/sty/color.sty new file mode 100644 index 0000000..1422b33 --- /dev/null +++ b/sks/color/sty/color.sty @@ -0,0 +1,16 @@ +\usepackage[table]{xcolor} + +\definecolor{Gray}{gray}{.85} +\definecolor{LightGray}{gray}{.95} +\definecolor{White}{gray}{1} +\definecolor{Red}{rgb}{1,0,0} +\definecolor{Darkred}{rgb}{.5,0,0} +\definecolor{Lightred}{rgb}{0.855,0.812,0.812} +\definecolor{Metalightred}{rgb}{0.598,0.568,0.568} +\definecolor{RoyalBlue}{rgb}{0,0,0.5} +\definecolor{Pink}{rgb}{1,.8,.8} +\definecolor{Green}{rgb}{.8, 1, .8} +\definecolor{Bluu}{rgb}{.6,.7,.8} +\definecolor{Middlegray}{gray}{.5} + +\definecolor{LinkBlue}{rgb}{0.1,0.2,0.5} diff --git a/sks/date/date.k b/sks/date/date.k new file mode 100755 index 0000000..ebfb014 --- /dev/null +++ b/sks/date/date.k @@ -0,0 +1,23 @@ +@@date.k : Date formatted as "16 June 1910" @@ +@@date.html :: @eval time.strftime("%d %B %Y").lstrip('0') eval@ @@ +@@date.tex :: @eval time.strftime("%d %B %Y").lstrip('0') eval@ @@ +@@date.txt :: @eval time.strftime("%d %B %Y").lstrip('0') eval@ @@ + +@@datetime.k : Date formatted as "16 June 1910, 13:10 @@ +@@datetime.html :: @eval time.strftime("%d %B %Y, %H:%M").lstrip('0') eval@ @@ +@@datetime.tex :: @eval time.strftime("%d %B %Y, %H:%M").lstrip('0') eval@ @@ + +@@timestamp.k : Date and time formatted as "1910.06.16-12:34" @@ +@@timestamp :: @eval time.strftime("%Y.%m.%d-%H:%M") eval@ @@ + +@@serialdate.k : Date formatted as "YYMMDD" @@ +@@serialdate :: @eval time.strftime("%y%m%d") eval@ @@ + +@@year.k : Current year formatted as "20XX" @@ +@@year :: @eval time.strftime("%Y") eval@ @@ + +#[ +@@@category date +date datetime timestamp serialdate +:desc Time and date formatting @@@ +]# diff --git a/sks/document/.gitignore b/sks/document/.gitignore new file mode 100644 index 0000000..a438335 --- /dev/null +++ b/sks/document/.gitignore @@ -0,0 +1 @@ +*.d diff --git a/sks/document/Makefile b/sks/document/Makefile new file mode 100644 index 0000000..d708aa6 --- /dev/null +++ b/sks/document/Makefile @@ -0,0 +1,58 @@ +# Klammertext sks/document/ Makefile +# Improved version with automatic header dependency tracking + +K := $(KLAMMERTEXT_HOME) +KS := $(K)/sks +KM := $(K)/mac + +include $(KM)/env/makefile.env + +# Additional include paths for sks components +LOCAL_CPPFLAGS := -I$(KM) -I$(KS)/kutil -I$(KS)/target + +# Shared library location +LIBDIR := $(K)/lib +LIBRARY := $(LIBDIR)/libklammertext.so + +# Linker flags for document.so +DOC_LDFLAGS := $(SHARED) $(EXPORT_DYNAMIC) -Wl,-rpath,'$(ORIGIN)/../../lib' -L$(LIBDIR) + +# Local object files +LOCAL_OBJECTS := document_class.o document_html.o document_latex.o heading.o reference.o +LOCAL_DEPFILES := document_class.d document_html.d document_latex.d heading.d reference.d document.d + +# External object files from sks/ (mac/*.o now in libklammertext.so) +SKS_OBJECTS := $(KS)/kutil/kutil.o $(KS)/kutil/klammer_base.o \ + $(KS)/target/html_util.o $(KS)/target/latex_util.o \ + $(KS)/target/font_resolve.o + +ALL_OBJECTS := $(LOCAL_OBJECTS) $(SKS_OBJECTS) + +# Compiler flags for dependency generation +DEPFLAGS = -MMD -MP -MF $(@:.o=.d) + +.PHONY: all clean deps + +# Default target +all : document.so + +# Build external dependencies first +deps : + $(MAKE) -C $(KS)/kutil + $(MAKE) -C $(KS)/target + +# Pattern rule for object files +%.o : %.cpp + @echo Compiling $< + $(CXX) -c $(CPPFLAGS) $(LOCAL_CPPFLAGS) $(CXXFLAGS) $(DEPFLAGS) $< -o $@ + +# Shared object - link against libklammertext.so +document.so : $(LOCAL_OBJECTS) $(SKS_OBJECTS) $(LIBRARY) document.cpp | deps + $(CXX) $(CPPFLAGS) $(LOCAL_CPPFLAGS) $(CXXFLAGS) $(DOC_LDFLAGS) $(LDFLAGS) \ + document.cpp -o $@ $(ALL_OBJECTS) -lklammertext $(LDLIBS) + +clean : + rm -f $(LOCAL_OBJECTS) $(LOCAL_DEPFILES) *.so *~ __pycache__ + +# Include generated dependency files (if they exist) +-include $(LOCAL_DEPFILES) diff --git a/sks/document/css/document.css b/sks/document/css/document.css new file mode 100644 index 0000000..542a45f --- /dev/null +++ b/sks/document/css/document.css @@ -0,0 +1,453 @@ + +:root { + --hpad: 1rem; + --frame_weight: normal; +} + + +html, body, div { + margin: 0; + padding: 0; +} + +body { + overflow: hidden; +} + +#title { + color: var(--frame_text_color); + background-color: var(--frame_background_color); + display: flex; + justify-content: space-between; + align-items: center; + overflow: hidden; + font-family: var(--sans-serif); + font-size: 1.4rem; + font-weight: var(--frame_weight); + padding: .4rem 1rem; + overflow: hidden; + border-top: 1px solid black; + border-bottom: 1px solid black; +/* text-shadow: 1px 1px 2px black; */ +} + +#middle { + display: flex; + padding: 0; + margin: 0; + border: 0; + align-content: stretch; + overflow: hidden; +} + +/* In plain/article, #content is the only child of #middle and must + fill the full width. In book, #content shares #middle with #toc + and #resizer, so this rule must not apply. */ +#content:only-child { + flex: 1; +} + + +#navtools, #searchtools { + display: inline; + vertical-align: middle; + margin: 0; + padding: 0; + border: 0; + white-space: nowrap; +} + +#searchtools { +} + + +#nav { + color: var(--nav_text_color); + background-color: var(--nav_background_color); + font-family: var(--sans-serif); + height: 1.4rem; + padding: .35rem 1rem; + font-size: .9rem; + line-height: 1.2rem; + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid black; + font-weight: var(--frame_weight); +} + +.navb { + padding: 0 .25em 0 .25em; + cursor: pointer; + user-select: none; +} + +.navb:first-child { + padding: 0 .25em 0 0; +} + +#search_input { + width: 25vw; + max-width: 12rem; + height: .9rem; + font-family: var(--sans-serif); + font-size: .8rem; + margin: 0; + border: 1px solid black; + padding-left: 0.25rem; +} + +#search_input:focus { + outline-width: 0; +} + + +#search_box { + display: inline-block; + margin: 0; + padding: 0; + border: 0; + line-height: 0; +} + +.search_target { + background-color: rgb(95%,95%,60%); + padding: 0 .25em 0 .25em; + border: 1px solid rgb(70%,70%,70%) +} + +/* +#help { + padding-top: .2rem 0 .15rem 0; +} +*/ + +#text, #help_page, #search_page { + color: black; + background-color: white; + padding: 0 1.5rem 1.5rem 1.5rem; + overflow: auto; + height: 100%; +} + + +#status { + color: var(--frame_text_color); + background-color: var(--frame_background_color); + font-family: var(--sans-serif); + font-size: .8rem; + font-weight: var(--frame_weight); + padding: .25rem 0 .35rem 0; + line-height: 1.2rem; + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: center; + border-top: 1px solid black; + border-bottom: 1px solid black; +/* text-shadow: 1px 1px 2px black; */ +} + +.spacer { + display: inline-block; + width: .7em; +} + + +.endspace { + height: 50vh; +} + +.tight { + border: 0; + margin: 0; + padding: .1em 0 .1em 0; +} + + +#xtoc { + font-family: var(--sans-serif); + width: auto; + cursor: default; + float: left; + white-space: nowrap; + position: absolute; + overflow: auto; + overflow-x: hidden; + background-color: lightyellow; + padding: 0; + line-height: 1.5; + font-size: .9rem; +} + +.navlink { + cursor: pointer; +} + +.navlink:hover .section-title { + text-decoration: underline; + text-underline-offset: 0.2em; + text-decoration-thickness: 1px; +} + +#slider { + z-index: 90; + overflow: visible; +} + +#pagetoc { + font-family: var(--sans-serif); + line-height: 1.2; +} + +#resizer { + border-width: 0px 1px 0px 1px; + border-color: transparent black transparent black; + border-style: solid; + background-color: var(--nav_background_color); + height: 100%; + width: 4px; + cursor: e-resize; + } + +#toc { + background-color: white; + font-family: var(--sans-serif); + font-size: .8rem; + height: 100%; + overflow-y: auto; + overflow-x: hidden; + white-space: nowrap; + position: relative; + padding: 1rem 0 0 1.8rem; + + box-sizing: border-box; +} + +#toc, #toc_items ul, #toc_items ul li { + list-style-type: none; +} + +#toc ul { + list-style-type: none; + padding-left: .5rem; + margin: 0; +} + +#toc .caret { + padding-left: .65rem; +} + +#toc li { + line-height: 1.5; +} + +.toctitle { + font-family: var(--sans-serif); + font-size: 1.2rem; + margin-top: 1rem; +} + +.toc { + font-family: var(--sans-serif); + font-size: 1.1rem; + margin-top: .1rem; +} + +.tocnumber{ + padding-right: .75rem; +} + +.tab0 { padding-left: 1rem; } +.tab1 { padding-left: 2rem; } +.tab2 { padding-left: 3rem; } +.tab3 { padding-left: 4rem; } +.tab4 { padding-left: 5rem; } +.tab5 { padding-left: 6rem; } +.tab6 { padding-left: 7rem; } +.tab7 { padding-left: 8rem; } +.tab8 { padding-left: 9rem; } +.tab9 { padding-left: 10rem; } + +/* +#toc ul { + padding-left: .5rem; + line-height: 1.5; +} + +#toc li::marker { + font-size: 0px; +} + +#toc_items { + margin: 0; + padding: 1em; + line-height: 1.4; +} + +#toc_items li { + padding-left: 0; +} + +#toc_items ul .unnumbered { + padding-left: 0; +} + +*/ + +.caret { + cursor: pointer; + user-select: none; + font-size: .8rem; + padding: 0 .3rem 0 0; + vertical-align: middle; + /* + color: gray; + + */ + font-weight: bold; +} + +.caret::before { + display: inline-block; + content: "\25B7"; + transform: translateY(-.4ex); +} + +.caret-down::before { + transform: translateY(-.2ex) rotate(90deg); +} + +.nested { + display: none; +} + +.active { + display: block; +} + +.highlight { + font-weight: bold; + padding: 0; + margin: 0; +} + +.highlight_parent { + background-color: var(--nav_background_color); + border: solid 1px var(--nav_background_color); +} + + +.section-title { + padding: 0; + border: 0; +} + +.caret { + margin-left: -1.75rem; + border: 0; +} + +.section-number { + padding-right: .3rem; +} + +#pagecache { + display: none; +} + +#search_page { + background-color: var(--nav_background_color); +} + +.search_result { + border: solid 1px black; + padding: .75rem; + margin: 1rem 0 1rem 0; + color: black; + text-decoration: none; + background-color: white; + line-height: 1.3; + display: block; +} + +.search_result:hover { + text-decoration: none; +} + +.search_result p, search_result div { + margin: 0; + padding: 0; +} + + + +#text { + background-color: white; + padding-bottom: 2rem; +} + + +#search_clear_input_box { + display: inline-block; + width: 1.5rem; + border: 0; + cursor: pointer; + user-select: none; +} + +#fit { + margin: 0 .25em 0 .5em; + user-select: none; +} + +#linkshow { + margin: 0 0 0 .25em; +} + +input[type="checkbox"] { + cursor: pointer; + user-select: none; +} + +label { + user-select: none; +} + +label[for=linkshow_check], label[for=fit_check] { + cursor: pointer; +} + +.displayed_link { + /* background-color: var(--nav_background_color); */ +/* background-color: rgb(209,255,127, .5); */ + background-color: rgb(70%,80%,100%,.5); + text-decoration: none; /* underline; */ + color: black; +/* padding: 0 .25em 0 .25em; */ +} + +.undisplayed_link { + background-color: transparent; + color: var(--link-blue); + text-decoration: none; +} + + +.footeritem { + padding: 0 .75em; + white-space: nowrap; +} + +.no_spacing * { + margin: 0 !important; + padding: 0 !important; + vertical-align: middle; +} + +/* +p::before { + content: "[" attr(id) "] "; +} +*/ diff --git a/sks/document/css/list.txt b/sks/document/css/list.txt new file mode 100644 index 0000000..2d33d4e --- /dev/null +++ b/sks/document/css/list.txt @@ -0,0 +1 @@ +document.css diff --git a/sks/document/document.cpp b/sks/document/document.cpp new file mode 100644 index 0000000..38bb22c --- /dev/null +++ b/sks/document/document.cpp @@ -0,0 +1,86 @@ +#include "document_class.h" +#include "latex_util.h" +#include "machine.h" +#include "show.h" +#include "log.h" + +// extern "C" is needed for dlsym name lookup. Returning std::string from +// C-linkage functions is technically non-standard but works correctly on +// Linux (Itanium ABI) where C and C++ calling conventions are identical. +#if defined(__clang__) +#pragma clang diagnostic ignored "-Wreturn-type-c-linkage" +#endif + +extern "C" +std::string document(Machine& machine) +{ + (void)K::log(3); + try { + Document_class D(machine); + return D.result(); + } + catch (Error& err) { + err.print_message(); + std::cout << "\n"; + exit(1); + } +} + +extern "C" +std::string tex_to_pdf(Machine& machine) +{ + (void)K::log(3); + try { + check_for_xelatex(); + std::string outbase = machine.m_state.value("K_output_dir") + + "/" + machine.m_state.value("K_output_basename"); + std::string tex_filename = outbase + ".tex"; + for (auto ext : {"aux", "log", "out", "toc"}) { + fs::remove(outbase + "." + ext); + } + std::string command = "xelatex -interaction=batchmode -halt-on-error " + tex_filename; + string_to_file(tex_filename, machine.m_result); + std::string xelatex_output = exec(command.c_str()); + std::string xelatex_log = string_from_file(outbase + ".log"); + std::vector error_lines = find_latex_error_lines(xelatex_log); + if (!error_lines.empty()) { + std::stringstream ss {}; + ss << "Errors reported in xelatex log file:\n"; + for (auto line : error_lines) { + ss << " " << line << "\n"; + } + ss << "Check log file: " << outbase << ".log"; + throw Definition_error(ss.str(), Locator(), false); + } + if (std::regex_search(xelatex_log, std::regex("Package rerunfilecheck Warning:"))) { + (void)K::log(1, "Rerunning xelatex because document structure has changed"); + xelatex_output = exec(command.c_str()); + xelatex_log = string_from_file(outbase + ".log"); + error_lines = find_latex_error_lines(xelatex_log); + if (!error_lines.empty()) { + std::stringstream ss {}; + ss << "Errors reported in xelatex log file:\n"; + for (auto line : error_lines) { + ss << " " << line << "\n"; + } + ss << "Check log file: " << outbase << ".log"; + throw Definition_error(ss.str(), Locator(), false); + } + } + /* + if (std::stoi(machine.m_state.value("K_verbose_level")) < 2) { + for (auto ext : word_split("tex out aux log toc")) { + fs::remove(outbase + "." + ext); + } + } + */ + return ""; + } + catch (Error& err) { + std::cout << red; + err.print_message(); + std::cout << black << "\n"; + exit(1); + } +} + diff --git a/sks/document/document.k b/sks/document/document.k new file mode 100644 index 0000000..102d6ea --- /dev/null +++ b/sks/document/document.k @@ -0,0 +1,122 @@ +@@@argtype document_structure | + Structure of a document: plain, article, or book + :pattern plain^|article^|book +@@@ + +@@document.k +:title +:subtitle +:page_title +:author + +:date +:version +:logo +:favicon + +#[ +:nav.bool false +:toc.bool false +]# + +:structure.document_structure plain + +:text +:files +:cache.bool true + +:css_text +:css_files +:frame_background_color black +:frame_text_color white +:nav_background_color rgb(80%,85%,90%) +:nav_text_color black +:js_text +:js_files + +:include_sks_css.bool true +:include_sks_js.bool true +:include_fonts.bool true + +:local_fonts.list linux-libertine +:google_fonts.list Inconsolata Open+Sans:wght^@500 Libre+Baskerville + +:serif_font Crimson Pro +:sans_font Open Sans +:mono_font Inconsolata +:font_scale 1.0 + +:landscape.bool.tex false +:paper_size..tex a4paper +:leading.float.tex 1.05 +:point_size.int.tex 11 +:ragged_right.bool.tex false +:cover..tex +:prolog..tex +:two_column.bool.tex false +:copyright +:bottom +:use_pages_dir.bool.html true +:create_output_directory.bool true +:output_directory_name +:resources_in_file.bool false +: Top-level document structure +@@ + +@@document :: @eval :cpp *KLAMMERTEXT_HOME*/sks/document/document document @ @@ + +# @link https://cdn.britannica.com/09/152309-050-5E0B2A42/Sahara-Morocco.jpg :text Niagara @ + +@@niagara : +@link https://cdn.britannica.com/09/152309-050-5E0B2A42/Sahara-Morocco.jpg :text Niagra @ +@@ + +#[ +@@niagara.html : +Niagara +@@ + +@@niagara.tex : +@link https://cdn.britannica.com/09/152309-050-5E0B2A42/Sahara-Morocco.jpg :text Niagra @ +@@ +]# + + +# @@moby : Moby @@ +@@moby : +But here is an artist. He desires to paint you the dreamiest, +shadiest, quietest, most enchanting bit of romantic landscape in all +the valley of the Saco. What is the chief element he employs? There +stand his trees, each with a hollow trunk, as if a hermit and a +crucifix were within; and here sleeps his meadow, and there sleep his +cattle; and up from yonder cottage goes a sleepy smoke. Deep into +distant woodlands winds a mazy way, reaching to overlapping spurs of +mountains bathed in their hill-side blue. But though the picture lies +thus tranced, and though this pine-tree shakes down its sighs like +leaves upon this shepherd's head, yet all were vain, unless the +shepherd's eye were fixed upon the magic stream before him. Go visit +the Prairies in June, when for scores on scores of miles you wade +knee-deep among Tiger-lilies---what is the one charm wanting? Water +---there is not a drop of water there! Were @niagara@ but a cataract of +sand, would you travel your thousand miles to see it? +@@ + +@@smoby.html : +Go visit the Prairies in June, when for scores on scores of miles you +wade knee-deep among Tiger-lilies---what is the one charm wanting? +Water --- there is not a drop of water there! Were @niagara@ but a +cataract of sand, would you travel your thousand miles to see it? +@@ + +@@smoby.tex : +Go visit the Prairies in June, when for scores on scores of miles you +wade knee-deep among Tiger-lilies---what is the one charm wanting? +Water --- there is not a drop of water there! Were @niagara@ but a +cataract of sand, would you travel your thousand miles to see it? +@@ + +@@ssmoby : +Go visit the Prairies in June, when for scores on scores of miles you +wade knee-deep among Tiger-lilies---what is the one charm wanting? +Water---there is not a drop of water there! +@@ diff --git a/sks/document/document.py b/sks/document/document.py new file mode 100644 index 0000000..97f700e --- /dev/null +++ b/sks/document/document.py @@ -0,0 +1,23 @@ +import kutil +import html_util +import tex_util + +class Document(klammer_base.Klammer_base): + def __init__(self, K): + super().__init__(K) + + def html(self): + if self.include_css == "true": + css_code, css_files = html_util.css(self.css, self._sks_enabled) + head = html_util.head(self.title, css_files=css_files, css_code=css_code) + else: + head = head = html_util.head(self.title, include_fonts=self.include_fonts == "true") + return html_util.page(head, self.body) + + def tex(self): + ragged_right = self.ragged_right in {"true", "True", "1"} + return tex_util.page( + self.title, self.subtitle, self.leading, self.pointsize, ragged_right, self.body) + + def txt(self): + return self.body diff --git a/sks/document/document_class.cpp b/sks/document/document_class.cpp new file mode 100644 index 0000000..466173b --- /dev/null +++ b/sks/document/document_class.cpp @@ -0,0 +1,182 @@ +#include "util.h" +#include "machine.h" +#include "error.h" +#include "kutil.h" +#include "html_util.h" +#include "latex_util.h" +#include "locator.h" +// #include "state.h" +#include "document_class.h" +#include "show.h" +#include "log.h" +#include "file.h" + +bool strbool(std::string s, Locator loc) +{ + std::vector values = {"false", "False", "0", "true", "True", "1"}; + if (is_not_in(s, values)) { + std::stringstream ss {}; + ss << "The value \"" << s << "\" is not a Boolean values. Possible values are:\n" + << join(values, ", "); + throw Argument_error(ss.str(), loc, false); + } + bool result = (find(values.begin(), values.end(), s) - values.begin()) > 2; + return result; +} + + +Document_class::Document_class(Machine& machine) : Klammer_base(machine) +{ + (void)K::log(3); + // show("document in main SKS"); + m_machine = machine; + sloc = get("K_loc"); + m_title = get("title"); + m_subtitle = get("subtitle"); + m_page_title = get("page_title"); + m_author = get("author"); + m_date = get("date"); + + // m_nav = strbool(get("nav"), loc); + // m_toc = strbool(get("toc"), loc); + + m_structure = docstruct(get("structure")); + + m_date = get("date"); + m_version = get("version"); + m_logo = get("logo"); + + m_text = get("text"); + m_files = word_split(get("files")); + + m_css_text = get("css_text"); + m_css_filenames = word_split(get("css_files")); + m_include_sks_css = strbool(get("include_sks_css"), loc); + frame_background_color = get("frame_background_color"); + frame_text_color = get("frame_text_color"); + nav_background_color = get("nav_background_color"); + nav_text_color = get("nav_text_color"); + + js_text = get("js_text"); + m_js_filenames = word_split(get("js_files")); + m_include_sks_js = strbool(get("include_sks_js"), loc); + + // font_dirs = word_split(get("font_dirs")); + m_local_fonts = word_split(get("local_fonts")); + m_google_fonts = word_split(get("google_fonts")); + + auto strip_quotes = [](std::string s) { + if (s.size() >= 2 && s.front() == '"' && s.back() == '"') + s = s.substr(1, s.size() - 2); + return s; + }; + m_serif_font = strip_quotes(get("serif_font")); + m_sans_font = strip_quotes(get("sans_font")); + m_mono_font = strip_quotes(get("mono_font")); + m_font_scale = stof(get("font_scale")); + + if (!m_serif_font.empty()) + m_resolved_serif = resolve_font(m_serif_font); + if (!m_sans_font.empty()) + m_resolved_sans = resolve_font(m_sans_font); + if (!m_mono_font.empty()) + m_resolved_mono = resolve_font(m_mono_font); + paper_size = get("paper_size"); + landscape = strbool(get("landscape"), loc); + leading = stof(get("leading")); + point_size = stoi(get("point_size")); + ragged_right = strbool(get("ragged_right"), loc); + cover = get("cover"); + prolog = get("prolog"); + m_two_column = strbool(get("two_column"), loc); + + m_copyright = get("copyright"); + m_bottom = get("bottom"); + + create_output_directory = strbool(get("create_output_directory"), loc); + output_directory_name = get("output_directory_name"); + + resources_in_file = strbool(get("resources_in_file"), loc); + + //use_pages_dir = strbool(get("use_pages_dir"), loc); + + // m_toc_only = strbool(get("K_toc_only"), loc); + m_kt_root_filename = get("K_input_filenames"); + + m_no_cache = !strbool(get("cache"), loc); + + write_files = !strbool(get("K_stdout_only"), loc); + m_input_dir = get("K_input_dir"); + + if (m_text.size() == 0 and m_files.size() == 0) { + throw Argument_error( + "Neither the :text or :files options have values"); //, + //get("K_loc")); + } + /* + for (std::string file : m_files) { + sources.push_back(string_from_file(find_kt_file(file))); + } + */ + // std::cout << "IN DOCUMENT CLASS:\n" << m_state << "\n"; +} + + +void Document_class::save_string_input_as_file() +{ + if (!m_text.empty()) { + std::string input_text_filename = m_cache_dir + "/_text.kt"; + // msg() << "Write string input to cache file: " + // << input_text_filename << "\n"; + string_to_file(input_text_filename, m_text); + m_files.insert(m_files.begin(), input_text_filename); + } +} + + +fs::path parse_input_filename(std::string s, std::string input_dir) +{ + fs::path p(s); + if (p.extension() != ".kt") { + p += ".kt"; + } + if (!file_exists(p)) { + p = input_dir + "/kt/" + p.string(); + } + return p; +} + +void Document_class::write(std::string filename, std::string contents) +{ + write_file(filename, contents, write_files); +} + + +void write_file(std::string filename, std::string contents, bool write_p) +{ + if (write_p) { + string_to_file(filename, contents); + } else { + msg() << "Output filename: " << filename << "\n"; + } +} + + +/* +void output_msg(std::string msg) +{ + std::cout << red; + (void)K::log(1, msg); + std::cout << black; +} + +void Document_class::create_directory(std::string directory) +{ + if (write_files) { + fs::create_directories(directory); + } else { + output_msg("Output directory: " + directory); + } +} + +*/ diff --git a/sks/document/document_class.h b/sks/document/document_class.h new file mode 100644 index 0000000..308fcf1 --- /dev/null +++ b/sks/document/document_class.h @@ -0,0 +1,184 @@ +#pragma once + +#include "klammer_base.h" +#include "html_util.h" +#include "latex_util.h" +#include "machine.h" +#include "heading.h" +#include "font_resolve.h" + +const std::string closed_symbol { "&^#9656;" }; +const std::string open_symbol { "&^#9662;" }; +inline std::map html_tags { + {"kt-part", 0}, {"kt-chapter", 1}, {"h1", 2}, + {"h2", 3}, {"h3", 4}, {"h4", 5}, + {"h5", 6}, {"h6", 7}, {"h7", 8}}; + +using string_pairs_t = std::vector>; + +std::string unescape_newlines(std::string s); + +enum class docstruct_t { + plain, + article, + book +}; + +inline docstruct_t docstruct(std::string name) +{ + if (name == "plain") { + return docstruct_t::plain; + } else if (name == "article") { + return docstruct_t::article; + } else { + return docstruct_t::book; + } +} + +class Document_class : public Klammer_base { +public: + Document_class(Machine& machine); + ~Document_class() = default; + + void save_string_input_as_file(); + + //void create_directory(std::string directory); + void write(std::string filename, std::string contents); + + /* + strings_t get_css_files( + std::string output_dir, + std::string css_text, strings_t css_files, bool write_file); + */ + + strings_t get_js_files( + bool nav, bool include_sks_js, + std::string pages_basenames_filename); // , std::string output_dir, bool write_file); + + argmap m_args {}; + void write_basenames_js_file(std::string filename, std::vector basenames); + std::string create_html_output_directories(); + std::string create_tex_output_directories(); + + std::pair, std::vector> + html_auxiliary_files(std::string output_directory, std::string pages_basename_filename, + std::vector custom_css_files); + + + void process_input_files(); + std::vector insert_section_numbers(std::string marker="___NUM___", bool add_to_toc=true); + void insert_html_caption_numbers(); + std::vector> reference_list(std::string target_marker); + std::map id_to_caption_number(); + std::map>> html_references(); + void resolve_html_references(); + std::string single_page_toc(); + elements_t page(std::string body, std::string output_dir, int max_level); + + std::string make_single_html_page(std::string output_directory); + std::string make_html_navigation_structure( + std::string output_directory, std::vector headings); + std::string html(); + std::string tex(); + std::string make_help_page(); + std::string color_definitions(); + std::string font_definitions(); + strings_t resolved_font_names(); + + Machine m_machine {}; + + std::string m_title {}; + std::string m_subtitle {}; + std::string m_page_title {}; + std::string m_author {}; + std::string m_date {}; + std::string m_version {}; + std::string m_logo {}; + + // bool m_nav = false; + // bool m_toc = false; + docstruct_t m_structure = docstruct_t::plain; + + std::string m_text {}; + strings_t m_files {}; + + std::string m_output_dir {}; + + bool include_css = true; + bool include_js = true; + + std::string m_css_text {}; + strings_t m_css_filenames {}; + bool m_include_sks_css = true; + std::string frame_background_color {}; + std::string frame_text_color {}; + std::string nav_background_color {}; + std::string nav_text_color {}; + + std::string js_text {}; + strings_t m_js_filenames {}; + bool m_include_sks_js = true; + + strings_t m_font_dirs {}; + strings_t m_local_fonts {}; + strings_t m_google_fonts {}; + + std::string m_serif_font {}; + std::string m_sans_font {}; + std::string m_mono_font {}; + float m_font_scale = 1.0f; + Resolved_font m_resolved_serif {}; + Resolved_font m_resolved_sans {}; + Resolved_font m_resolved_mono {}; + + std::string prolog {}; + std::string paper_size {}; + bool m_two_column = false; + bool landscape = false; + float leading = 1.0; + int point_size = 10; + bool ragged_right = false; + std::string cover {}; + + std::string m_copyright {}; + std::string m_bottom {}; + + bool create_output_directory = true; + std::string output_directory_name {}; + bool resources_in_file = false; + //bool use_pages_dir = true; + + bool m_toc_only = false; + std::string m_kt_root_filename {}; + + bool m_no_cache = false; + + bool write_files = true; + std::string m_input_dir {}; + + // strings_t sources {}; + Locator loc {}; + std::string sloc {}; + + bool m_html_only = false; + + std::vector> m_file_components {}; + std::vector> m_string_components {}; + std::vector> m_toc_components {}; + + std::string m_cache_dir {}; +}; + +fs::path parse_input_filename(std::string s, std::string input_dir); +void write_file(std::string filename, std::string contents, bool write_files); + +/* +std::vector combine_files( + std::vector filenames, std::string output_filename, + std::string prolog="", std::string epilog="", + bool write_file_p=true, + std::function processor=nullptr); +*/ + + + diff --git a/sks/document/document_html.cpp b/sks/document/document_html.cpp new file mode 100644 index 0000000..6c245b9 --- /dev/null +++ b/sks/document/document_html.cpp @@ -0,0 +1,771 @@ +#include "util.h" +#include "error.h" +#include "log.h" +#include "kutil.h" +#include "html_util.h" +#include "heading.h" +#include "document_class.h" +#include "character.h" +#include "reference.h" +#include "show.h" +#include "file.h" + +std::string unescape_newlines(std::string s) +{ + return string_replace(s, " ___NL___ ", "\n"); + +} + +int ID = 0; +std::string BASE {}; + +static const std::regex newline_rgx(R"(\n)"); +static const std::regex element_open_rgx(R"(<([\w-]+)(\s+.*?)?>)"); +static const std::regex id_attr_rgx("id=\"(.*?)\""); + + +std::string insert_missing_ids(std::smatch match) +{ + (void)K::log(3); + std::string result = match[0]; + std::string tag = match[1]; + if (html::tag_is_block_element(tag)) { + std::string attrs = match[2]; + std::stringstream ss {}; + if (attrs.find(" id=") == std::string::npos) { + ss << "id=\"_e" << ID++<< "\""; + } + ss << attrs; + if (!BASE.empty()) { + ss << " data-basename=\"" << BASE << "\""; + } + result = "<" + tag + " " + trim(ss.str()) + ">"; + } + return result; +} + +std::string insert_ids(std::string html_text) +{ + (void)K::log(3); + std::string result = html_text; + result = freplace(result, element_open_rgx, insert_missing_ids); + return result; +} + +std::string Document_class::make_help_page() +{ + (void)K::log(3); + std::string kt_filename = klammertext_filename("doc/html_help.kt"); + std::string basename = "help"; + std::string help_page; + //if (!in_modification_order(kt_filename, output_filename, no_cache)) { + if (cache_requires_update(m_cache_dir, kt_filename, basename)) { + Machine Mh = m_machine; + Mh.read(fs::path(klammertext_filename("doc/html_help.kt"))); + help_page = Mh.apply("html"); + help_page += "\n
\n"; + help_page = html::make_paragraphs(help_page); + write_to_cache(m_cache_dir, basename, help_page); + } else { + help_page = read_from_cache(m_cache_dir, basename); + } + return help_page; +} + +std::string Document_class::color_definitions() +{ + std::stringstream ss {}; + ss << ":root {\n" + << " --frame_background_color: " << frame_background_color << ";\n" + << " --frame_text_color: " << frame_text_color << ";\n" + << " --nav_background_color: " << nav_background_color << ";\n" + << " --nav_text_color: " << nav_text_color << ";\n}\n"; + return ss.str(); +} + +strings_t Document_class::resolved_font_names() +{ + strings_t result {}; + auto add = [&](const Resolved_font& rf) { + if (!rf.family_name.empty() && + std::find(m_local_fonts.begin(), m_local_fonts.end(), rf.dir_name) == m_local_fonts.end()) + result.push_back(rf.dir_name); + }; + add(m_resolved_serif); + add(m_resolved_sans); + add(m_resolved_mono); + return result; +} + +std::string Document_class::font_definitions() +{ + std::stringstream ss {}; + if (!m_serif_font.empty() || !m_sans_font.empty() || !m_mono_font.empty()) { + ss << ":root {\n"; + if (!m_serif_font.empty()) + ss << " --serif: " << m_serif_font << ", serif;\n"; + if (!m_sans_font.empty()) + ss << " --sans-serif: " << m_sans_font << ", sans-serif;\n"; + if (!m_mono_font.empty()) + ss << " --monospace: " << m_mono_font << ", monospace;\n"; + ss << "}\n"; + } + // Emit scale factors so sans and mono fonts match the serif font. + // Three scaling methods (uncomment the desired one): + // x-height: serif_xh / other_xh (matches lowercase, like fontspec MatchLowercase) + // cap-height: serif_ch / other_ch (matches capitals) + // average: mean(serif_xh,serif_ch) / mean(other_xh,other_ch) (compromise) + float serif_xh = m_resolved_serif.xheight_ratio; + float serif_ch = m_resolved_serif.capheight_ratio; + float serif_avg = (serif_xh + serif_ch) / 2.0f; + if (serif_avg > 0.0f) { + auto scale = [&](const Resolved_font& other) -> std::string { + float other_avg = (other.xheight_ratio + other.capheight_ratio) / 2.0f; + if (other_avg > 0.0f && other_avg != serif_avg) { + char buf[16]; + // float ratio = serif_xh / other.xheight_ratio; // x-height + // float ratio = serif_ch / other.capheight_ratio; // cap-height + float ratio = serif_avg / other_avg; // average + std::snprintf(buf, sizeof(buf), "%.4f", ratio); + return buf; + } + return ""; + }; + std::string sans_scale = scale(m_resolved_sans); + std::string mono_scale = scale(m_resolved_mono); + if (!sans_scale.empty() || !mono_scale.empty()) { + ss << ":root {\n"; + if (!sans_scale.empty()) + ss << " --sans-serif-scale: " << sans_scale << ";\n"; + if (!mono_scale.empty()) + ss << " --monospace-scale: " << mono_scale << ";\n"; + ss << "}\n"; + } + } + // Global font scale: applied to body font-size + if (m_font_scale != 1.0f) { + char buf[16]; + std::snprintf(buf, sizeof(buf), "%.4f", m_font_scale); + ss << "body { font-size: " << buf << "rem; }\n"; + } + return ss.str(); +} + +void Document_class::write_basenames_js_file(std::string filename, std::vector basenames) +{ + (void)K::log(3); + std::string tab(" "); + std::stringstream ss {}; + ss << "function pages_basenames()\n{\n return [\n"; + int i = 0; + int last = basenames.size() - 1; + for (std::string base : basenames) { + ss << tab << "\"" << base << "\""; + if (i < last) { + ss << ","; + } + ss << "\n"; + i++; + } + ss << " ];\n}\n"; + write(filename, ss.str()); +} + +std::string Document_class::create_html_output_directories() +{ + (void)K::log(3); + std::string output_directory = + absolute_pathname(get("K_output_dir")) + "/" + get("K_output_basename"); + if (!m_toc_only) { + if (!file_exists(output_directory)) { + fs::create_directory(output_directory); + } + html::install_local_fonts(m_font_dirs, m_local_fonts, output_directory); + install_resolved_font(m_resolved_serif, output_directory); + install_resolved_font(m_resolved_sans, output_directory); + install_resolved_font(m_resolved_mono, output_directory); + } + m_cache_dir = cache_directory("_html_pages"); + if (m_no_cache && fs::exists(m_cache_dir)) { + for (auto& entry : std::filesystem::directory_iterator(m_cache_dir)) { + msg() << " Removing cache directory: " << entry << "\n"; + std::filesystem::remove_all(entry); + } + } + if (!m_toc_only) { + if (!file_exists(output_directory, false, true)) { + fs::create_directory(output_directory); + } + } + return output_directory; +} + +strings_t get_css_files(strings_t custom_css_files) +{ + (void)K::log(3); + strings_t result = sks_files_of_type("css"); + result.insert(result.end(), custom_css_files.begin(), custom_css_files.end()); + return result; +} + + +std::vector js_basenames(bool nav) +{ + if (nav) { + return { "kutil", "document", "image", "resize_window", "resize_text", "resize_toc", + "toc", "level", "next_last", "help", "show_links", "search", "state", "code", "link" }; + } else { + return {"kutil", "document", "image"}; + } +} + +strings_t Document_class::get_js_files( + bool nav, bool include_sks_js, + std::string pages_basenames_filename) +{ + (void)K::log(3); + strings_t result {}; + if (include_sks_js) { + for (auto b : js_basenames(nav)) { + std::vector js_files = + find_file_recursive(klammertext_dir() + "/sks", b + ".js"); + if (js_files.size() == 0) { + throw File_error("File " + q_(b) + " not found.", Locator()); + } + result.push_back(js_files[0].string()); + } + if (!pages_basenames_filename.empty()) { + result.push_back(pages_basenames_filename); + } + } + return result; +} + + +strings_t link_filenames(strings_t& full_filenames, std::string page_dir) +{ + strings_t result {}; + fs::path page_dir_path(page_dir); + // int i = 0; + std::transform(full_filenames.begin(), full_filenames.end(), std::back_inserter(result), + [&](std::string f) { + fs::path full(f); + //msg() << i << full << " " << full.filename() << "\n"; + fs::path path(page_dir_path / full.filename()); + //std::cout << i++ << path << "\n"; + return path.string(); }); + return result; +} + +std::pair, std::vector> +Document_class::html_auxiliary_files(std::string output_directory, std::string pages_basenames_filename, + std::vector custom_css_files) +{ + (void)K::log(3); + strings_t all_css_files = custom_css_files; + strings_t css_link_filenames {}; + if (write_files && m_include_sks_css) { + all_css_files = get_css_files(m_css_filenames); + //output_dir, color_definitions(), css_files, write_files); + } + if (write_files) { + copy_preserving_basename( + all_css_files, output_directory, "css"); + css_link_filenames = link_filenames(all_css_files, "css"); + } + + strings_t all_js_files {}; + strings_t js_link_filenames {}; + if (write_files) { + bool nav = m_structure == docstruct_t::book; + all_js_files = get_js_files( + nav, m_include_sks_js, + pages_basenames_filename); // , output_dir, write_files); + copy_preserving_basename( + all_js_files, output_directory, "js"); + js_link_filenames = link_filenames(all_js_files, "js"); + } + + // msg() << "all_css_files:\n " << join(all_css_files, "\n ") << "\n\n"; + // msg() << "all_js_files:\n " << join(all_js_files, "\n ") << "\n\n"; + + return {css_link_filenames, js_link_filenames}; +} + + +void Document_class::process_input_files() +{ + (void)K::log(3); + m_file_components.clear(); + size_t basename_width = 0; + for (auto input_filename : m_files) { + fs::path inpath = input_filename; + if (!file_exists(inpath)) { + inpath = parse_input_filename(input_filename, m_input_dir); + } + std::string basename = file_basename(input_filename); + basename_width = std::max(basename_width, basename.size()); + std::string html_text; + //if (true || !in_modification_order(inpath, cached_filename, m_no_cache)) { + if (m_no_cache || cache_requires_update(m_cache_dir, inpath, basename)) { + Machine M = m_machine; + M.read(inpath); + html_text = trim(M.apply("html")); + html_text = trim(html::make_paragraphs(html_text)) + "\n"; + html_text = unescape_newlines(html_text); + BASE = basename; + html_text = insert_ids(html_text); + m_file_components.push_back({basename, html_text}); + write_to_cache(m_cache_dir, basename, html_text); + } else { + html_text = read_from_cache(m_cache_dir, basename); + m_file_components.push_back({basename, html_text}); + } + } + // for (auto [src, result] : m_file_components) { + // msg() << std::left << std::setw(basename_width) << src << " " << abbrev(result, 96) << "\n"; + // } +} + +std::string increment_level(int level, std::vector &levels) +{ + levels[level]++; + int zero_offset = level == 0 ? 2 : 1; + for (unsigned int i = level + zero_offset; i < levels.size(); i++) { + levels[i] = 0; + } + std::stringstream result {}; + if (level == 0) { + result << levels[0]; + } else { // Parts not included in numbering + for (auto i = 1; i <= level; i++) { + result << levels[i]; + if (i < level) { + result << "."; + } + } + } + // msg() << level << sp_arrow << result.str() << sp_arrow << "\n"; + return result.str(); +} + + +std::string extract_id(std::string attr) +{ + std::smatch match {}; + if (!std::regex_match(attr, match, id_attr_rgx)) { + throw Internal_error("Pattern for id attribute incorrect: " + q_(attr)); + } + return match[1]; +} + +std::vector +Document_class::insert_section_numbers(std::string marker, bool add_to_toc) +{ + std::vector levels(9, 0); + std::smatch match {}; + std::string pattern = R"(<([\w-]+)\s*(.*?)>(.*?)MARKER\s*\s*(.*?)<.*)"; + pattern = string_replace(pattern, "MARKER", marker); + std::regex heading_rgx(pattern); + std::vector> modified_components {}; + std::vector headings; + bool include_toc = m_structure != docstruct_t::plain; + for (auto [src, result] : m_file_components) { + std::stringstream modified {}; + for (std::string line : regex_split(result, newline_rgx, false)) { + if (std::regex_match(line, match, heading_rgx)) { + // xheading + std::string tag = match[1]; + std::string id = extract_id(match[2]); + std::string title = match[3]; + std::string heading = match[4]; + const int level = html_tags[match[1]]; + // msg() << line << sp_arrow << broken_bar << tag << broken_bar + // << level << broken_bar << "ATTR" << broken_bar << title << broken_bar + // << heading << broken_bar << "\n"; + std::string number = increment_level(level, levels); + line = string_replace(line, marker, number); + if (include_toc) { + line = "" + line + ""; + } + //msg() << attr << " " << number << " " << heading << "\n"; + if (add_to_toc) { + m_toc_components.push_back({tag, id, number, heading}); + } + Heading hd(level, src, number, id, number, heading); + headings.push_back(hd); + // msg() << line << "\n\n"; + + } + modified << line << "\n"; + } + modified_components.push_back({src, modified.str()}); + } + m_file_components = modified_components; + return headings; +} + +int get_caption_number(std::map& numbers, std::string label) +{ + int result; + if (numbers.count(label) == 0) { + result = 1; + numbers[label] = 2; + } else { + result = numbers[label]; + numbers[label]++; + } + return result; +} + +void Document_class::insert_html_caption_numbers() +{ + // msg() << "insert_html_caption_numbers\n"; + std::regex chapter_rgx(R"(.*sectionnumber">(\d+).*)"); + std::regex caption_rgx(R"(__CAPTION__(.*?)__CAPTION__(.*?)__CAPTION__)"); + std::string current_chapter {}; + std::map current_caption_number {}; + std::vector> modified_components {}; + + for (auto [src, result] : m_file_components) { + std::stringstream modified {}; + //std::vector references {}; + for (std::string line : regex_split(result, newline_rgx, false)) { + std::smatch chapter_match {}; + if (std::regex_match(line, chapter_match, chapter_rgx)) { + current_chapter = chapter_match[1]; + // msg() << "Chapter " << current_chapter << " " << line << "\n"; + current_caption_number.clear(); + } + // msg() << "Line: " << line << "\n"; + std::smatch caption_match {}; + if (std::regex_search(line, caption_match, caption_rgx)) { + std::string label = caption_match[1]; + std::string ctext = caption_match[2]; + int caption_number = get_caption_number(current_caption_number, label); + // msg() << " caption: " << current_chapter << "." << caption_number << "\n"; + // msg() << "Label: " << label << " Text: " << ctext << "\n"; + std::stringstream ss {}; + ss << label << " "; + if (!current_chapter.empty()) { + ss << current_chapter << "."; + } + ss << caption_number << ctext; + line = std::regex_replace(line, caption_rgx, trim(ss.str())); + // msg() << "line: " << line << "\n"; + // references.push_back(line); + } + // if (std::regex_search(line, reference_rgx)) { + // references.push_back(line); + // } + modified << line << "\n"; + } + modified_components.push_back({src, modified.str()}); + //m_reference_sequences.push_back(references); + } + m_file_components = modified_components; +} + + +std::vector> +Document_class::reference_list(std::string target_marker) +{ + std::regex reference_rgx("__REF__"); + std::regex target_rgx("
> references {}; + for (auto [src, result] : m_file_components) { + for (std::string line : regex_split(result, newline_rgx, false)) { + if (std::regex_search(line, reference_rgx)) { + references.push_back({target_marker, src, line}); + } else { + std::smatch match {}; + if (std::regex_search(line, match, target_rgx)) { + references.push_back({"", match[2], match[1]}); + } + } + } + } + return references; +} + +std::map Document_class::id_to_caption_number() +{ + std::regex target_rgx("
(\w+\s+[\w.]+))"); + std::map result {}; + for (auto [src, ftext] : m_file_components) { + auto lines = regex_split(ftext, newline_rgx, false); + for (unsigned int i = 0; i < lines.size(); i++) { + std::smatch match {}; + if (std::regex_search(lines[i], match, target_rgx)) { + std::string id = match[1]; + unsigned int j = i + 1; + std::smatch caption_match {}; + while (j < lines.size()) { + if (std::regex_search(lines[j], caption_match, caption_rgx)) { + std::string caption = caption_match[1]; + // msg() << id << sp_arrow << caption << "\n"; + result[id] = caption; + break; + } + j++; + } + } + } + } + return result; +} + + +std::pair offset_spec_to_count(std::string spec) +{ + std::regex rgx(R"((\w+)\s*(\d*))"); + std::smatch match {}; + int offset = 1; + int dir = 1; + if (std::regex_match(spec, match, rgx)) { + std::string desc = match[1]; + std::string value = match[2]; + if (!value.empty()) { + offset = std::stoi(value); + } + if (desc == "before") { + dir = -1; + } + } else { + throw Internal_error("Reference offset spec incorrect: " + spec); + } + return {offset, dir}; +} + +std::map>> +Document_class::html_references() +{ + std::string target_marker = "@"; + auto references = reference_list(target_marker); + // msg() << "\n"; + for (unsigned int i = 0; i < references.size(); i++) { + auto [mark, name, ref] = references[i]; + // msg() << " " << i << ": " << mark << " " << name << " " << ref << "\n"; + } + // msg() << "\n"; + auto id_captions = id_to_caption_number(); + + std::regex ref_rgx(R"(__REF__(.*?)__(.*?)__)"); + std::string offset_spec; + std::string type; + + std::map>> result {}; + + for (unsigned int i = 0; i < references.size(); i++) { + auto [mark, name, ref] = references[i]; + // msg() << " " << i << ": " << mark << " " << name << " " << ref << "\n"; + if (mark == target_marker) { + std::smatch match {}; + if (std::regex_search(ref, match, ref_rgx)) { + offset_spec = match[1]; + type = match[2]; + } else { + throw Internal_error("Reference form is incorrect: " + ref); + } + auto [offset, dir] = offset_spec_to_count(offset_spec); + int j = i + dir; + while (offset > 0 && j >= 0 && j < (int)references.size()) { + // msg() << " check: " << std::get<2>(references[j]) << "\n"; + if (std::get<1>(references[j]) == type) { + offset--; + } + if (offset == 0) { + break; + } + j += dir; + } + if (j < 0 || j >= (int)references.size()) { + // msg() << "Not found: " << std::get<2>(references[i]) << "\n"; + } else { + std::string id = std::get<2>(references[j]); + std::string line = std::get<2>(references[i]); + // msg() << "Ref: " << line << boldblack << " found: " << black << id << "\n"; + std::string link = "" + id_captions[id] + ""; + // msg() << link << "\n"; + std::string modified_line = std::regex_replace(line, ref_rgx, link); + // msg() << "modified_line: " << name << sp_arrow << modified_line << "\n"; + result[name].push_back({line, modified_line}); + } + } + } + return result; +} + +void Document_class::resolve_html_references() +{ + auto references = html_references(); + std::vector> modified_components {}; + for (auto [src, ftext] : m_file_components) { + // msg() << "resolve " << src << ":\n"; + std::string modified_text = ftext; + for (auto [old_line, new_line] : references[src]) { + // msg() << old_line << sp_arrow << new_line << "\n"; + modified_text = string_replace(modified_text, old_line, new_line); + } + modified_components.push_back({src, modified_text}); + } + m_file_components = modified_components; +} + +std::string Document_class::single_page_toc() +{ + //

2.1 Section two.one

+ std::regex id_rgx("id=\"(.*?)\""); + std::stringstream ss {}; + ss << "
Contents
\n"; + int minimum_depth = 7; + // Adjust left minimum margin based on whether parts or chapters are the top level: + for (auto [tag, id, number, heading] : m_toc_components) { + minimum_depth = std::min(minimum_depth, html_tags[tag]); + } + for (auto [tag, id, number, heading] : m_toc_components) { + int depth = html_tags[tag]; + // msg() << "TOC: " << depth << " " << id << " " << number << " " << heading << "\n"; + ss << "\n"; + } + // msg() << ss.str(); + return ss.str(); +} + + +elements_t Document_class::page(std::string body_text, std::string output_dir, int max_level) +{ + (void)K::log(3); + std::string toc {}; // Calculate + bool text_only = m_structure == docstruct_t::plain; + + // msg() << "output_dir: " << output_dir << "\n"; + + auto [css_filenames, js_filenames] = + html_auxiliary_files(output_dir, "", m_css_filenames); + + std::string toc_text {}; + + elements_t body {}; + html::add_title(body, m_title, m_logo); + if (m_structure == docstruct_t::book) { + html::add_nav(body, max_level); + } + html::add_text(body, body_text, toc_text, text_only); + // html::add_bottom_spacer(body); + html::add_status_bar(body, m_date, m_version, m_copyright); + if (!text_only) { + html::add_page_cache(body); + } + html::add_js_links(body, js_filenames, output_dir) ; + + std::string css_text = color_definitions() + font_definitions() + "\n" + m_css_text; + + std::string page_title = m_page_title.empty() ? m_title : m_page_title; + + strings_t all_local_fonts = m_local_fonts; + for (auto& name : resolved_font_names()) + all_local_fonts.push_back(name); + + elements_t page = html::make_page( + body, page_title, + css_text, css_filenames, all_local_fonts, m_google_fonts); + + return page; +} + +std::string Document_class::make_single_html_page(std::string output_directory) +{ + (void)K::log(3); + std::stringstream ss {};; + + if (m_structure != docstruct_t::plain) { + ss << single_page_toc(); + } + for (auto [basename, page_text] : m_file_components) { + ss << "\n\n" << page_text << "\n"; + } + std::string single_page = to_string(page(ss.str(), output_directory, 10)); + std::string output_file = output_directory + "/index.html"; + // msg() << "single_page to " << output_file << "\n"; + string_to_file(output_file, single_page); + return ""; + +} + +std::string Document_class::make_html_navigation_structure( + std::string output_directory, std::vector headings) +{ + msg() << "Navigation format\n"; + std::vector pages_basenames {}; + + for (auto [basename, page_text] : m_file_components) { + pages_basenames.push_back(basename); + msg() << basename << ": " << abbrev(page_text, 96) << "\n"; + } + std::string pages_basenames_filename = m_cache_dir + "/_pages_basenames.js"; + write_basenames_js_file(pages_basenames_filename, pages_basenames); + std::string help_content = make_help_page(); + + // Build embedded pagecache to avoid Same Origin Policy errors with file:// protocol + std::stringstream pagecache; + pagecache << "
\n"; + for (auto [basename, page_text] : m_file_components) { + pagecache << "
" + << page_text << "
\n"; + } + pagecache << "
" << help_content << "
\n"; + pagecache << "
\n"; + pagecache << "
\n"; + + std::pair toc_spec = make_navigation_table_of_contents(headings); + int max_level = std::get<0>(toc_spec); + std::string toc_body = std::get<1>(toc_spec); + std::string nav_body = "Navigation"; + auto [all_css_files, all_js_files] = + html_auxiliary_files(output_directory, pages_basenames_filename, m_css_filenames); + std::string css_text = color_definitions() + font_definitions() + "\n" + m_css_text; + std::stringstream t {}; + t << html::page( + m_title, m_page_title, output_directory, nav_body, max_level, toc_body, + m_file_components[0].second, + m_date, m_version, m_copyright, + css_text, all_css_files, all_js_files, + [&]() { strings_t f = m_local_fonts; + for (auto& n : resolved_font_names()) f.push_back(n); + return f; }(), + m_google_fonts, + m_logo); + std::string result = t.str(); + result = string_replace(result, + "
", + pagecache.str()); + std::string output_basename = m_machine.m_state.value("K_output_basename"); + std::string output_filename = output_directory + "/index.html"; + write(output_filename, result); + return ""; +} + + +std::string Document_class::html() +{ + (void)K::log(3); + // msg() << "HTML document\n"; + // m_cache_dir = cache_directory(kt_root_filename, "_html_pages"); + std::string output_directory = create_html_output_directories(); + save_string_input_as_file(); + process_input_files(); + auto headings = insert_section_numbers(); + insert_section_numbers("___NUM2___", false); + insert_html_caption_numbers(); + resolve_html_references(); + if (m_structure != docstruct_t::book) { + make_single_html_page(output_directory); + } else { + make_html_navigation_structure(output_directory, headings); + } + return ""; +} diff --git a/sks/document/document_latex.cpp b/sks/document/document_latex.cpp new file mode 100644 index 0000000..31b94be --- /dev/null +++ b/sks/document/document_latex.cpp @@ -0,0 +1,85 @@ +#include "util.h" +#include "kutil.h" +#include "log.h" +#include "document_class.h" +#include "reference.h" +#include "file.h" +#include "show.h" + +std::string Document_class::create_tex_output_directories() +{ + (void)K::log(3); + std::string output_directory = + absolute_pathname(get("K_output_dir")); // + "/" + get("K_output_basename"); + m_cache_dir = cache_directory("_latex_files"); + if (!m_toc_only) { + if (!file_exists(output_directory, false, true)) { + fs::create_directory(output_directory); + } + } + return output_directory; +} + +std::string Document_class::tex() +{ + (void)K::log(3); + create_tex_output_directories(); + // :text content is already processed (klammers applied by the outer Machine). + // Use it directly; don't re-process through a sub-Machine. + std::string latex_text {}; + if (!m_text.empty()) { + latex_text = add_latex_caption_numbers(m_text); + } + if (m_files.size() > 0) { + for (auto input_filename : m_files) { + std::string basename = file_basename(input_filename); + std::string cached_filename = m_cache_dir + "/" + basename; + fs::path inpath = parse_input_filename(input_filename, m_input_dir); + // if (!in_modification_order(input_filename, cached_filename, m_no_cache)) { + if (m_no_cache || cache_requires_update(m_cache_dir, inpath, basename)) { + Machine M = m_machine; + M.m_state.set("document_landscape", landscape ? "true" : "false"); // , "Landscape mode"); + M.read(inpath); + std::string processed = trim(M.apply("tex", false)); + processed = add_latex_caption_numbers(processed); + write_to_cache(m_cache_dir, basename, processed); + latex_text += processed; + } else { + latex_text += read_from_cache(m_cache_dir, basename); + } + } + } + auto split_lines = regex_split(latex_text, std::regex(R"(\n)"), false); + latex_text = resolve_caption_references("latex", split_lines, latex_line_states(split_lines)); + std::map section_to_id {}; + std::regex section_id_rgx(R"(%__sectionlink__(.*?)__(.*?)__)"); + for (std::string id_section : find_all(latex_text, section_id_rgx, 0)) { + std::smatch match {}; + std::regex_match(id_section, match, section_id_rgx); + section_to_id[match[2]] = match[1]; + } + latex_text = std::regex_replace(latex_text, section_id_rgx, ""); + std::regex seclink_rgx("(.*?)__seclink__(.*?)__(.*)"); + auto lines = regex_split(latex_text, std::regex(R"(\n)"), false); + for (auto i = 0ul; i < lines.size(); i++) { + if (lines[i].find("__seclink__") != std::string::npos) { + std::smatch match {}; + std::regex_match(lines[i], match, seclink_rgx); + std::stringstream ss {}; + ss << match[1] << section_to_id[match[2]] << match[3]; + lines[i] = ss.str(); + } + } + latex_text = join(lines, "\n"); + latex_text = string_replace(latex_text, "___AT___", "^@"); + std::string structure_name = + m_structure == docstruct_t::book ? "book" : + m_structure == docstruct_t::article ? "article" : "plain"; + return latex::page(structure_name, + m_title, m_subtitle, m_author, m_date, m_version, m_copyright, + m_bottom, prolog, paper_size, m_two_column, leading, + point_size, ragged_right, + cover, landscape, latex_text, + m_resolved_serif, m_resolved_sans, m_resolved_mono, + m_font_scale); +} diff --git a/sks/document/fonts/linux-libertine.css b/sks/document/fonts/linux-libertine.css new file mode 100644 index 0000000..9af200b --- /dev/null +++ b/sks/document/fonts/linux-libertine.css @@ -0,0 +1,21 @@ + +@font-face { + font-family: 'Linux Libertine'; + font-style: normal; + font-weight: 400; + src: url('linux-libertine/LinLibertine_Rah.ttf') format('truetype'); +} + +@font-face { + font-family: 'Linux Libertine'; + font-style: italic; + font-weight: 400; + src: url('linux-libertine/LinLibertine_RIah.ttf') format('truetype'); +} + +@font-face { + font-family: 'Linux Libertine'; + font-style: normal; + font-weight: bold; + src: url('linux-libertine/LinLibertine_RBah.ttf') format('truetype'); +} diff --git a/sks/document/fonts/linux-libertine/GPL.txt b/sks/document/fonts/linux-libertine/GPL.txt new file mode 100644 index 0000000..b890e55 --- /dev/null +++ b/sks/document/fonts/linux-libertine/GPL.txt @@ -0,0 +1,343 @@ + GNU GENERAL PUBLIC LICENSE (with font exception) + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + +As a special exception, if you create a document which uses this font, and embed this font or unaltered portions of this font into the document, this font does not by itself cause the resulting document to be covered by the GNU General Public License. This exception does not however invalidate any other reasons why the document might be covered by the GNU General Public License. If you modify this font, you may extend this exception to your version of the font, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. + diff --git a/sks/document/fonts/linux-libertine/LICENCE.txt b/sks/document/fonts/linux-libertine/LICENCE.txt new file mode 100644 index 0000000..dd8b672 --- /dev/null +++ b/sks/document/fonts/linux-libertine/LICENCE.txt @@ -0,0 +1,7 @@ +- Lizenz / Licence - + +Unsere Schriften sind frei im Sinne der GPL, d.h. (stark vereinfacht) dass Veränderungen an der Schriftart erlaubt sind unter der Bedingung, dass diese wieder der Öffentlichkeit unter gleicher Lizenz freigegeben werden. Querdenker behaupten oft, dass bei der Verwendung einer GPL-Schrift eingebettet in beispielsweise eine PDF auch diese freigestellt werden müsse. Deshalb gibt es die sogenannte "Font-exception" der GPL (welche diesem Lizenztext hinzugefügt wurde). Weitere Informationen zur GPL (Lizenztext mit Font-Exzeption als GPL.txt in diesem Paket). +Zusätzlich stehen die Schriften unter der Open Font License (siehe OFL.txt). + +Our fonts are free in the sense of the GPL. In short: Changing the font is allowed as long as the derivative work is published under the same licence again. Pedantics keep claiming that the embedded use of GPL-fonts in i.e. PDFs requires the free publication of the PDF as well. This is why our GPL contains the so called "font exception". Further information about the GPL (licence text with font exception see GPL.txt in this package). +Additionally our fonts are licensed under the Open Fonts License (see OFL.txt). \ No newline at end of file diff --git a/sks/document/fonts/linux-libertine/LinLibertine_RBah.ttf b/sks/document/fonts/linux-libertine/LinLibertine_RBah.ttf new file mode 100644 index 0000000..7eaeb0f Binary files /dev/null and b/sks/document/fonts/linux-libertine/LinLibertine_RBah.ttf differ diff --git a/sks/document/fonts/linux-libertine/LinLibertine_RIah.ttf b/sks/document/fonts/linux-libertine/LinLibertine_RIah.ttf new file mode 100644 index 0000000..d0c800c Binary files /dev/null and b/sks/document/fonts/linux-libertine/LinLibertine_RIah.ttf differ diff --git a/sks/document/fonts/linux-libertine/LinLibertine_Rah.ttf b/sks/document/fonts/linux-libertine/LinLibertine_Rah.ttf new file mode 100644 index 0000000..e1dc224 Binary files /dev/null and b/sks/document/fonts/linux-libertine/LinLibertine_Rah.ttf differ diff --git a/sks/document/fonts/linux-libertine/OFL-1.1.txt b/sks/document/fonts/linux-libertine/OFL-1.1.txt new file mode 100644 index 0000000..8aed073 --- /dev/null +++ b/sks/document/fonts/linux-libertine/OFL-1.1.txt @@ -0,0 +1,94 @@ +Copyright (c) 2003–2012, Philipp H. Poll (www.linuxlibertine.org | gillian at linuxlibertine.org), +with Reserved Font Name "Linux Libertine" and "Biolinum". + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/sks/document/heading.cpp b/sks/document/heading.cpp new file mode 100644 index 0000000..0e9dfd3 --- /dev/null +++ b/sks/document/heading.cpp @@ -0,0 +1,320 @@ +#include + +#include "util.h" +#include "file.h" +#include "html_util.h" +#include "heading.h" +#include "log.h" +#include "show.h" + +std::ostream& operator<<(std::ostream& os, const Heading h) +{ + os << h.m_level << " " + << h.m_filename << " " + << h.m_section << " " + << h.m_id << " " + << h.m_number << " " + << h.m_title; + return os; +} + +std::string levels_to_section(std::vector levels) +{ + (void)K::log(3); + std::string result {}; + auto count = levels.size(); + for (unsigned int i = 0; i < count; i++) { + if (levels[i] == 0) + break; + result += std::to_string(levels[i]); + if (i < count - 1) + result += "."; + } + if (count > 1) + result = result.substr(0, result.size()-1); + return result; +} + +int part_number = 1; +int unnumbered_id = 0; + +/* +std::tuple, std::vector, unsigned int> +add_section_numbers(std::string s, std::string basename, std::vector levels, unsigned int initial_id, + std::map& section_id_map) +{ + (void)K::log(3); + auto depth { levels.size() }; + std::vector headings {}; + std::string text { s }; + std::regex section_rgx (R"((.*?)(.*?))"); + std::regex part_rgx (R"((.*?)Part (\d+)\s*
\s*(.*?)\s*)"); + std::regex id_rgx(R"((.*?)id=\"([-\w]+)\"(.*))"); + + unsigned int id_number { initial_id }; + //std::string result {}; + std::stringstream result {}; + for (std::string line : regex_split(s, std::regex(R"(\n)"), false)) { + std::smatch match {}; + if (std::regex_match(line, match, part_rgx)) { + std::string pre { match[1] }; + std::string attr { match[2] }; + std::string level { match[3] }; + std::string title { match[4] }; + + std::string id_prefix { "_part_"}; + std::string id {}; + std::smatch id_match {}; + if (std::regex_match(attr, id_match, id_rgx)) { + id = id_match[2]; + } else { + id = id_prefix + std::to_string(part_number); + } + //title = "Part " + std::to_string(part_number) + " - " + title; + //std::string section = "Part " + level; + std::string section = "Part " + std::to_string(part_number); + elements_t part_title + { html::elt("kt-part", + { html::elt("span", section).attr("class", "sectionnumber"), + html::elt("span", trim(title)).attr("class", "sectiontitle") }) + .attr("id", id) + .attr("data-level", level) }; + result << pre << part_title << "\n"; + headings.push_back(Heading(0, basename, section, id, "0", title)); + part_number += 1; + } else if (std::regex_match(line, match, section_rgx)) { + std::string pre { match[1] }; + int level { std::stoi(match[2]) }; + std::string attr { match[3] }; + std::string title { match[4] }; + bool numbered = attr.find("numbered") != std::string::npos; + std::string section {}; + if (numbered) { + levels[level-1] = levels[level-1] + 1; + for (unsigned int li = level; li < depth; li++) + levels[li] = 0; + section = levels_to_section(levels); + } + std::string id {}; + std::smatch id_match {}; + if (std::regex_match(attr, id_match, id_rgx)) { + id = id_match[2]; + } else { + if (section.size() == 0) { + id = "_su_" + std::to_string(unnumbered_id++); + } else { + id = "_s_" + string_replace(section, ".", "_"); + } + } + std::string link_target = file_basename(basename) + link_delimiter + id; + section_id_map[title] = link_target; + + //

+ // 1Image tests

+ + elements_t title_parts {}; + if (numbered) { + title_parts.push_back(html::elt("span", trim(section)).attr("class", "sectionnumber")); + } + title_parts.push_back(html::elt("span", trim(title)).attr("class", "sectiontitle")); + + // msg() << "title_parts: " << title_parts << "\n"; + elements_t section_title + { html::elt("h"+std::to_string(level), title_parts) + .attr("id", id) + .attr("data-level", std::to_string(level))}; + // msg() << pre << section_title << "\n"; + result << pre << section_title << "\n"; + headings.push_back(Heading(level, basename, section, id, "0", title)); + } else { + result << line << "\n"; + } + } + //std::cout << "add_section_numbers: " << result.str() << " " + //<< headings.size() << " " << levels.size() << " " << id_number << "\n"; + return std::tuple(result.str(), headings, levels, id_number); +} +*/ + +std::string make_html_table_of_contents(std::vector headings) +{ + elements_t toc { html::elt("h1", "Contents") }; + for (auto [level, filename, section, id, number, title] : headings) { + toc.push_back( + html::elt("div", + html::elt("a", { + html::elt("span", section).attr("class", "level_number"), + title }) + .attr("href", "^#" + id) + .attr("class", "level")) + .attr("class", "level" + std::to_string(level))); + } + //auto toc_elt = html::elt("div", toc).attr("id", "toc"); + std::string result { "
\n" + to_string(toc) + "\n
\n" }; + return result; +} + +std::string toc_link_attrs( + std::string section, std::string title, std::string filename, std::string target, int level) +{ + std::string basename = file_basename(filename); + std::stringstream ss {}; + ss << "class=\"navlink\" data-page=\"" << basename + << "\" data-target=\"" << target << "\">\n\n"; + if (section.size() > 0) { + ss << "" << section << "\n"; + } + ss << "" << title << "\n" + << "\n"; + return ss.str(); +} + + +std::pair +make_navigation_table_of_contents(std::vector headings) +{ + std::stringstream toc {}; + long unsigned int i = 1; + //int max_level = 0; + //int last_level = 0; + int max_level = 1; + int last_level = 1; + + toc << "
    \n"; + int min_level = 7; + for (auto h : headings) { + min_level = std::min(h.m_level, min_level); + } + + bool unnumbered_start = headings.size() > 0 ? headings[0].m_section.empty() : false; + if (unnumbered_start) { // Unnumbered first section (preface, for example) + toc << "
      \n"; + } + for (auto [item_level, filename, section, id, number, title] : headings) { + // msg() << " level: " << item_level << "; filename: " << filename << "; section: " << section + // << "; id: " << id << "; number: " << number << "; title: " << title << "\n"; + + bool promote_unnumbered = unnumbered_start && min_level == 0 and section.empty(); + if (promote_unnumbered) { + item_level--; + } + std::string basename = file_basename(filename); + max_level = std::max(max_level, item_level); + //int next_level = i < headings.size() ? headings[i].m_level : 0; + + int next_level = i < headings.size() ? headings[i].m_level : 1; + + if (promote_unnumbered) { + next_level = std::max(0, next_level - 1); + } + + // std::cout << "|" << item_level << "|" << section << "|" << id << "|" + // << title << "| " << "next: " << next_level << "\n"; + + std::string link = toc_link_attrs(section, title, basename, id, item_level); + + if (next_level == item_level) { + toc << "
    • \n"; + /* + } else if (next_level > item_level) { + toc << "
    • " + << "\n" + << "
        \n"; + */ + } else if (next_level > item_level) { + toc << "
      • \n" + << "\n" + << "
          \n"; + } else { + //toc << "
        • \n
        \n
      • \n"; + toc << "
      • \n"; + /* + if (unnumbered_start) { + next_level += 1; + } + */ + int offset = unnumbered_start ? 1 : 0; + //int offset = unnumbered_start ? 3 : 2; + offset = 0; + + for (int lev = item_level; lev > next_level + offset; lev--) { + toc << "
      \n
    • \n"; + } + unnumbered_start = false; + } + i++; + last_level = item_level; + } + toc << "
    \n"; + for (int j = 0; j < last_level - 2; j++) { + toc << "\n
\n"; + } + return {max_level, toc.str()}; +} + +void show_table_of_contents( + std::string kt_root_filename, std::string title, std::vector headings) +{ + long unsigned int width = kt_root_filename.size(); + for (Heading h : headings) { + width = std::max(width, h.m_filename.size()); + } + width += 2; + int tab = 2; + std::string current_filename = ""; + std::string bar = " | "; + std::cout << "\n" << std::string(width - kt_root_filename.size(), ' ') + << kt_root_filename << bar << title << "\n"; + for (auto [level, filename, section, id, number, htitle] : headings) { + std::string f = ""; + if (filename != current_filename) { + f = std::string(width - filename.size(), ' ') + filename + bar; + current_filename = filename; + } else { + f = std::string(width, ' ') + bar; + } + std::cout << f << std::string((level-1) * tab, ' ') << section << " " << htitle << "\n"; + } + std::cout << "\n"; +} + +std::string cache_table_of_contents( + std::string kt_root_filename, std::string title, std::vector headings) +{ + long unsigned int width = kt_root_filename.size(); + for (Heading h : headings) { + width = std::max(width, h.m_filename.size()); + } + width += 2; + int tab = 2; + std::string current_filename = ""; + std::string bar = " | "; + std::stringstream ss {}; + ss << "\n" << std::string(width - kt_root_filename.size(), ' ') + << kt_root_filename << bar << title << "\n"; + for (auto [level, filename, section, id, number, htitle] : headings) { + std::string f = ""; + if (filename != current_filename) { + f = std::string(width - filename.size(), ' ') + filename + bar; + current_filename = filename; + } else { + f = std::string(width, ' ') + bar; + } + ss << f << std::string((level) * tab, ' '); + if (section.size() > 0) { + ss << section << " "; + } + ss << htitle << "\n"; + } + ss << "\n"; + std::string cache_filename = + cache_directory(kt_root_filename, "_html_pages") + "/toc.txt"; + string_to_file(cache_filename, ss.str()); + return cache_filename; +} diff --git a/sks/document/heading.h b/sks/document/heading.h new file mode 100644 index 0000000..ee61376 --- /dev/null +++ b/sks/document/heading.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include + +const std::string link_delimiter { "%" }; + +class Heading { +public: + Heading(int level, std::string filename, std::string section, std::string id, + std::string number, std::string title) + : m_level(level), m_filename(filename), m_section(section), m_id(id), + m_number(number), m_title(title) + {}; + int m_level {}; + std::string m_filename {}; + std::string m_section {}; + std::string m_id {}; + std::string m_number {}; + std::string m_title {}; +}; + +std::ostream& operator<<(std::ostream& os, const Heading h); + +/* +std::tuple, std::vector, unsigned int> +add_section_numbers(std::string s, std::string basename, std::vector levels, unsigned int initial_id, + std::map& section_id_map); +*/ + +std::string make_html_table_of_contents(std::vector headings); + +std::pair +make_navigation_table_of_contents(std::vector headings); + +void show_table_of_contents( + std::string kt_filename, std::string title, std::vector headings); + +std::string cache_table_of_contents( + std::string kt_root_filename, std::string title, std::vector headings); diff --git a/sks/document/js/document.js b/sks/document/js/document.js new file mode 100644 index 0000000..649ed75 --- /dev/null +++ b/sks/document/js/document.js @@ -0,0 +1,45 @@ + +function show_navigation_items(show_item) { + show_item = show_item === undefined ? true : false; + K.getv(".depthchoice").forEach(function (o) { + //K.visible(o, "hide"); + K.get(o).hidden = !show_item; + }); + ["textshow", "next", "previous", "fit", "linkshow"].forEach(function (o) { + K.get("#"+o).hidden = !show_item; + }); +} + + +function get_height(name) { + let div = K.get(name); + let result = 0; + if (div !== null) { + result = K.height(div); + } + return result; +} + +function frame_resize() { + let nav = K.get("#nav"); + let nav_height = (nav ? get_height(nav) : 0); + let content_height = window.innerHeight + - get_height("#title") - nav_height - get_height("#status") + 1; + K.height("#middle", content_height); +} + + + +window.addEventListener("load", function (event) { + window.addEventListener("resize", function (e) { + frame_resize(); + }); + + frame_resize(); + if (typeof resize_toc === "function") resize_toc(true); + + //let endspace_height = "calc(110% - " + K.height("#title") + // + " - " + K.height("#nav") + ")"; + //K.style(".endspace", "height", endspace_height); + +}); diff --git a/sks/document/js/help.js b/sks/document/js/help.js new file mode 100644 index 0000000..dac74f4 --- /dev/null +++ b/sks/document/js/help.js @@ -0,0 +1,33 @@ +function modify_buttons_for_help(status) { + K.visible("#linkshow", status); + K.visible("#textshow", status); + K.visible("#searchtools", status); + K.get("#help").textContent = status == "show" ? "Help" : "Back"; +} + +function show_help() { + let header = K.get("#text h1"); + if (header && header.textContent === "Navigation features") { + textshow(); + modify_buttons_for_help("show"); + frame_resize(); + } else { + save_position(); + K.get("#text").replaceWith(K.get("#help_page").cloneNode(true)); + K.attr("#help_page", "id", "text"); + K.visible("#toc", "hide"); + K.visible("#resizer", "hide"); + set_buttons("hide"); + resize_text_width(); + modify_buttons_for_help("hide"); + frame_resize(); + } +} + +window.addEventListener("load", function (event) { + K.get("#help").addEventListener("click", function (e) { + event.preventDefault(); + show_help(); + }); + define_keypress("H", "#help"); +}); diff --git a/sks/document/js/level.js b/sks/document/js/level.js new file mode 100644 index 0000000..76a2fdb --- /dev/null +++ b/sks/document/js/level.js @@ -0,0 +1,51 @@ +/* Open up the table of contents to a specified level in the hierarchy: */ + +/* +function show_all() { + let level_buttons = K.getv(".depthchoice"); + level_buttons[level_buttons.length-1].click(); +} + +function show_highlighted(node) { + //let carets = K.getv(".caret"); + let node_caret = node.parentNode.previousElementSibling; + console.log(node_caret); +} +*/ + +window.addEventListener("load", function (event) { + + K.map(".depthchoice", function (depthchoice) { + depthchoice.addEventListener("click", function (event) { + //save_position(); + let max_depth = K.text(event.srcElement); + K.map(".caret", function (e) { + let level = K.attr(e, "data-level"); + let open = K.has_class(e, "caret-down"); + if (level < max_depth) { + if (!open) { + e.click(); + } + } else { + if (open) { + e.click(); + } + } + + }); + //restore_position(); + }); + }); + + K.get("html").addEventListener("keypress", function (event) { + const max_depth = K.getv(".depthchoice").length; + const zero = 48; // Hah! + let depth = 1; + for (depth = 1; depth <= max_depth; depth += 1) { + if (event.which === zero + depth) { + K.get('.depthchoice[data-depth="' + depth + '"]').click(); + } + } + }); + highlight_headers_in_toc(); +}); diff --git a/sks/document/js/list.txt b/sks/document/js/list.txt new file mode 100644 index 0000000..536773c --- /dev/null +++ b/sks/document/js/list.txt @@ -0,0 +1,12 @@ +fold.js +load.js +toc.js +resize_toc.js +document.js +resize_text.js +show_links.js +level.js +next_last.js +help.js +search.js +state.js diff --git a/sks/document/js/load.js b/sks/document/js/load.js new file mode 100644 index 0000000..dd5105b --- /dev/null +++ b/sks/document/js/load.js @@ -0,0 +1,135 @@ + +function first_basename() { + let first = K.get(".navlink"); + return first ? K.attr(first, "data-page") : null; +} + +function scroll_to_target(target_name) { + var text = K.get("#text"); + if (!text) return; + var folder_top = text.getBoundingClientRect().top; + var target = K.get(target_name); + if (target) { + var target_top = target.getBoundingClientRect().top; + var scroll_position = text.scrollTop + target_top - folder_top; + text.scrollTop = scroll_position; + } + highlight_visible_toc_sections(); +} + +function modify_links() { + var links = K.getv("#text a"); + links.forEach(function(link) { + if (link.classList.contains("linkd") || link.querySelector("img")) { + return; + } + var href = link.getAttribute("href"); + if (!href) return; + if (href.indexOf("http://") != -1 || + href.indexOf("https://") != -1 || + href.indexOf(".pdf") != -1 || + href.indexOf("mailto:") != -1 || + href.indexOf("/") == 0 || + href.indexOf("#") != -1 || + href.indexOf("..") == 0) { + return; + } + var parts = href.split('#'); + var len = parts[0].length; + var page = parts[0].substring(0, len - 5); + var target = "#" + parts[1]; + var span = document.createElement("span"); + span.className = "panelink"; + span.setAttribute("data-page", page); + span.setAttribute("data-target", target); + span.setAttribute("data-title", link.textContent); + span.textContent = link.textContent; + link.parentNode.replaceChild(span, link); + }); +} + + +function show_text() { + K.visible("#search_page", "hide"); + K.visible("#help_page", "hide"); + K.visible("#text", "show"); + K.style("#content", "background-color", "white"); + K.text("#help", "Help"); + highlight_visible_toc_sections(); +} + + +function new_page_callback() { + show_text(); + modify_links(); + resize(); + current_page_callback(); +} + +var image_timer; +var last_basename; + +function current_page_callback() { + if (target_name == "#") { + return; + } + var selector = '.navlink[data-target="' + (target_name.split("#")[1] || "") + '"]'; + var navlink = K.get(selector); + if (navlink) { + update_page_title(navlink); + } + resize(); + scroll_to_target(target_name); +} + + +async function load_page_content(basename, new_page, callback) { + if (true || basename != current_basename()) { + var url = "pages/" + basename + ".html"; + await K.load("#text", url); + callback(); + } + else { + callback(); + } +} + + +function load_page(basename, target, callback, push_state) { + var init_complete = false; + var push = typeof push_state !== "undefined"; + var state, state_target; + + if (init_complete) { + var parts = window.location.hash.toString().split("#"); + state = {"basename" : parts[1], "target" : "#" + parts[2]}; + state_target = "#" + basename + target; + if (push) { + history.pushState(state, "", state_target); + } else { + history.replaceState(state, "", state_target); + } + load_page_content( + basename, true, + function() { + target_name = target; + initialize = false; + callback(); } ); + + } else { + basename = first_basename(); + state = {"basename" : basename, "target" : "#"}; + state_target = "#" + basename + "#"; + + init_complete = true; + + load_page_content( + basename, true, + function() { + target_name = target; + initialize = true; + callback(); } ); + + history.replaceState(state, "", state_target); + } +} diff --git a/sks/document/js/next_last.js b/sks/document/js/next_last.js new file mode 100644 index 0000000..a5e2a6d --- /dev/null +++ b/sks/document/js/next_last.js @@ -0,0 +1,105 @@ +/* +Enable the "Next" and "Previous" buttons to move through the table of +contents, making entries visible if necessary. +*/ + +function first_highlighted_navlink() { + let highlighted_titles = [...K.getv(".section-title.highlight")]; + if (highlighted_titles.length === 0) { + return null; + } + return highlighted_titles[0].parentNode; +} + +function make_navlink_visible(navlink) { + const elts = [...K.getv(".navlink, .caret")]; + let i; + for (i = elts.indexOf(navlink); i >= 0; i -= 1) { + let elt = elts[i]; + if (K.has_class(elt, "caret")) { + if (!K.has_class(elt, "caret-down")) { + toggle_caret(elt); + } + if (K.attr(elt, "data-level") === "0") { + break; + } + } + } +} + +function show_button_status(id, active_p) { + //const inactive_color = "rgb(60%,60%,60%)"; + //const inactive_color = "rgb(20%,20%,20%)"; + const inactive_color = "transparent"; + //const text_color = "white"; + const text_color = "black"; + + const node = K.get(id); + if (active_p) { + K.style(node, "color", text_color); + //K.style(node, "color", "white"); + K.style(node, "text-decoration", "none"); + //K.style(node, "font-style", "normal"); + } else { + K.style(node, "color", inactive_color); + K.style(node, "text-decoration", "none"); + //K.style(node, "font-style", "italic"); + } +} + +function set_next_last_button_status() { + const navlinks = [...K.getv(".navlink")]; + const top = headers_in_text()[0][0]; + let status; + if (!top) { + status = [true, false]; + } + else if (top.id === K.attr(navlinks[0], "data-target")) { + status = [true, false]; + } else if (top.id === K.attr(navlinks[navlinks.length-1], "data-target")) { + status = [false, true]; + } else { + status = [true, true]; + } + show_button_status("#next", status[0]); + show_button_status("#previous", status[1]); +} + +function next_navlink() { + let navlinks = [...K.getv(".navlink")]; + let index = navlinks.indexOf(first_highlighted_navlink()); + if (index < navlinks.length - 1) { + let next = navlinks[index + 1]; + next.children[0].children[0].click(); + //make_navlink_visible(next); + } + return index + 1; +} + +function previous_navlink() { + let navlinks = [...K.getv(".navlink")]; + let index = navlinks.indexOf(first_highlighted_navlink()); + if (index > 0) { + let previous = navlinks[index - 1]; + previous.children[0].children[0].click(); + //make_navlink_visible(previous); + } + return index - 1; +} + +window.addEventListener("load", function (event) { + + K.get("#next").addEventListener("click", function (event) { + // hide_help(); + const index = next_navlink(); + resize_toc(); + }); + K.get("#previous").addEventListener("click", function (event) { + // hide_help(); + const index = previous_navlink(); + resize_toc(); + }); + define_keypress("N", "#next"); + define_keypress("P", "#previous"); + set_next_last_button_status(); +}); diff --git a/sks/document/js/resize_text.js b/sks/document/js/resize_text.js new file mode 100644 index 0000000..2a323d5 --- /dev/null +++ b/sks/document/js/resize_text.js @@ -0,0 +1,129 @@ +/* Toggling between text with the table of contents and the text without it */ + +function resize_text_width(toc_width) { + const displayed = displayed_text(); + const style = window.getComputedStyle(displayed, null); + const offset = parseInt(style.getPropertyValue("padding-left")) + + parseInt(style.getPropertyValue("padding-right")); + if (toc_width !== undefined) { + K.width(displayed, K.width("#middle") - toc_width - offset);//Sec - K.width("#resizer")); + } else { + /* + const style = window.getComputedStyle(displayed, null); + const offset = parseInt(style.getPropertyValue("padding-left")) + + parseInt(style.getPropertyValue("padding-right")); + */ + K.width(displayed, K.width("#middle") - offset); + } +} + +function get_top_header() { + return header_in_window("#text > p, #text > div"); +} + +function set_top_pos(top_header, original_top_pos) { + const new_top_pos = top_header.getBoundingClientRect().top; + const offset = original_top_pos - new_top_pos; + if (offset !== 0) { + //const text = K.get("#text"); + const text = displayed_text(); + text.scrollTo({top : text.scrollTop - offset}); + } +} + +function maintain_text_position(func) { + //const text_rect = K.get("#text").getBoundingClientRect(); + const text_rect = displayed_text().getBoundingClientRect(); + const top = text_rect.top - 10; + let top_elt; + let elt; + //for (let elt of K.get("#text").children) { + for (elt of displayed_text().children) { + if (K.top(elt) >= top) { + top_elt = elt; + break; + } + } + if (top_elt) { + let current_top = top_elt.getBoundingClientRect().top; + func.call(); + set_top_pos(top_elt, current_top); + } else { + func.call(); + } +} + + +function set_buttons(mode) { + let button; + for (button of ["#next", "#previous", "#fit"]) { + K.visible(button, mode); + } + for (button of K.getv(".depthchoice")) { + K.visible(button, mode); + } +} + +function textshow() { + if (K.visible("#toc")) { + save_position(); + K.visible("#toc", "hide"); + K.visible("#resizer", "hide"); + K.text("#textshow", "Table of contents"); + set_buttons("hide"); + resize_text_width(); + //resize_toc(); + resize_images(); + } else { + K.visible("#toc", "show"); + K.visible("#resizer", "show"); + K.text("#textshow", "Text only"); + + /* + let fit_active = K.attr("#fit_check", "active"); + if (fit_active === "false") { + K.get("#fit_check").click(); + } + */ + //resize_images(); + restore_position(); + set_buttons("show"); + resize_toc(); + resize_images(); + + // Restore previous state: + /* + if (fit_active === "false") { + K.get("#fit_check").click(); + } + */ + } + //resize_images(); + //adjust_widths(); + //center_elements(); + //set_code_comment_width(); + //set_imagecode_width(); + // adjust_footnotes(); +} + +window.addEventListener("load", function (event) { + + K.get("#textshow").addEventListener( + "click", + function (event) { + event.preventDefault(); + maintain_text_position(textshow); + }); + + window.addEventListener( + "resize", + function (event) { + maintain_text_position( + function () { + resize_text_width(); + resize_toc(); + //resize_images(); + }); + }); + define_keypress("T", "#textshow"); +}); diff --git a/sks/document/js/resize_toc.js b/sks/document/js/resize_toc.js new file mode 100644 index 0000000..5627f9f --- /dev/null +++ b/sks/document/js/resize_toc.js @@ -0,0 +1,110 @@ +/* +Resizing the pane of the table of contents to fit the titles, with +a draggable right edge to change the with of the table of contents pane. +*/ + +var resizer_width = 4; +var resizer_with_borders = 6; + +function current_full_toc_width() { + var width = 0; + K.map(".section-title-text", + function (elt) { + width = Math.max(width, K.right(elt)); + }); + // Add room for scrollbar: + //width += K.hpad("#toc_items") * 3.0 + resizer_width; + let caret = K.get(".caret"); + let caret_width = caret ? K.hpad(caret) : 16; // Need to adjust for now nested sections + width += K.hpad("#toc") + caret_width + K.hpad("#toc ul") +// + K.hpad("#toc_items") //* 1.5 // room for scroll bar + + resizer_width + 8; // Room for scroll bar + return width; +} + +function resize_toc(force_fit) { + if (K.visible("#toc") && (force_fit || K.attr("#fit_check", "active") === "true")) { + const toc_width = current_full_toc_width(); + const text_width = K.width("#middle") + - toc_width - resizer_with_borders; + //K.width("#text", text_width); + K.width(displayed_text(), text_width); + K.width("#toc", toc_width); + //resize_captions(); + + resize_images(); + + //K.height(".resizer", Math.max(K.height("#toc"), + // K.height("#toc_items"))); + //console.log("resize_toc: " + text_width, toc_width); + + /* + resize_images(); + adjust_widths(); + center_elements(); + set_annotate_max_width(); + set_code_comment_width(); + set_imagecode_width(); + */ + } +} + +function initResize(e) { + e.preventDefault(); + window.addEventListener("mousemove", Resize, false); + window.addEventListener("mouseup", stopResize, false); +} + +function Resize(e) { + e.preventDefault(); + // If fit is on, turn it off if the divider is moved manually. + if (K.attr("#fit_check", "active") == "true") { + K.get("#fit_check").click(); + } + + maintain_text_position( + function () { + //const toc_width = Math.max(0, e.clientX) + K.hpad("#text") + resizer_width; + const toc_width = Math.max(0, e.clientX) + K.hpad(displayed_text()) + resizer_width; + K.width("#toc", toc_width); + //K.width("#text", K.width("#middle") - toc_width); + K.width(displayed_text(), K.width("#middle") - toc_width); + resize_images(); + highlight_headers_in_toc(); + }); +} + +function stopResize(event) { + if (event) { + event.preventDefault(); + } + window.removeEventListener("mousemove", Resize, false); + window.removeEventListener("mouseup", stopResize, false); +} + +window.addEventListener("load", function (event) { + + K.get("#fit_check").addEventListener( + "click", + function (e) { + //console.log("fit_check click"); + maintain_text_position(resize_toc); + if (K.attr(e.srcElement, "active") === "false") { + K.attr(e.srcElement, "active", "true"); + } else { + K.attr(e.srcElement, "active", "false"); + } + resize_toc(); + localStorage.setItem("klammertext_fit", K.attr("#fit_check", "active")); + }); + + // The native