Typed arguments, calculated tables, spans, closed-world fonts, top-level fnt/ and env/
Sync with klammertext-dev through b90b0e09: - Argument types end to end: :python_cast values are applied (Python @eval receives real bools/numbers/lists), argument values are validated against their argtype patterns with the argtype's description as the error message, argtypes can declare :default (overridable per declaration), and parameterized type families are supported: rest(N) casts a rest argument to an N-dimensional list (bar-count = dimension). - Unified indexed_range syntax (selector with parenthesized subsets, composable mnemonic names) for table lines and spans. - Table klammer: caption fonts fixed in both targets, :column_width / :leading / :colsep wired, :colspan and :rowspan render (HTML attributes; \multicolumn / \multirow), calculated cell values (:calc) with prefix operators, display-precision semantics, :calc_format and :decimal period|comma. - Fonts: closed-world resolution on the Klammertext font store (infrastructure in mac/font_store; no Google Fonts links or fetch). Default fonts live in the top-level fnt/; additional fonts install into KLAMMERTEXT_FONTS directories via kdesc --font (list, samples, preview, install — classification by font metadata). CSS font family names are quoted (digit-initial families were silently lost). - Environment files moved from mac/env/ to the top-level env/; shell profiles source env/runtime.env. Dead per-host variants removed. - Container: fnt/ ships in the image; curl removed (no network use).
This commit is contained in:
@@ -5,7 +5,7 @@ K := $(KLAMMERTEXT_HOME)
|
|||||||
KS := $(K)/sks
|
KS := $(K)/sks
|
||||||
KM := $(K)/mac
|
KM := $(K)/mac
|
||||||
|
|
||||||
include $(KM)/env/makefile.env
|
include $(K)/env/makefile.env
|
||||||
|
|
||||||
# Commands to build
|
# Commands to build
|
||||||
COMMANDS := kdiag kdesc ktext
|
COMMANDS := kdiag kdesc ktext
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#include "command.h"
|
#include "command.h"
|
||||||
#include "error.h"
|
#include "error.h"
|
||||||
#include "file.h"
|
#include "file.h"
|
||||||
|
#include "font_store.h"
|
||||||
#include "ktype.h"
|
#include "ktype.h"
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
#include "argtype_set.h"
|
#include "argtype_set.h"
|
||||||
@@ -9,6 +10,52 @@
|
|||||||
#include "show.h"
|
#include "show.h"
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
|
|
||||||
|
static void font_usage()
|
||||||
|
{
|
||||||
|
std::cout <<
|
||||||
|
"Font maintenance commands:\n"
|
||||||
|
" --font List the available fonts\n"
|
||||||
|
" --font list The same\n"
|
||||||
|
" --font samples <output-dir> Write a sample page of the available fonts\n"
|
||||||
|
" to <output-dir>/index.html\n"
|
||||||
|
" --font samples <input-dir> <output-dir>\n"
|
||||||
|
" Write a sample page for the (not yet\n"
|
||||||
|
" installed) font files under <input-dir>\n"
|
||||||
|
" --font install <input-dir> Install the font files found under\n"
|
||||||
|
" <input-dir> into the user font directory\n"
|
||||||
|
" --font install <input-dir> <output-dir>\n"
|
||||||
|
" Install into <output-dir> instead\n"
|
||||||
|
" --font help This description\n"
|
||||||
|
"\n"
|
||||||
|
"Fonts are searched in the directories of the KLAMMERTEXT_FONTS\n"
|
||||||
|
"environment variable (colon-separated; default $HOME/.klammertext/fonts)\n"
|
||||||
|
"and then in the default font set.\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
static void font_command(const strings_t& words)
|
||||||
|
{
|
||||||
|
std::string verb = words.empty() ? "list" : words[0];
|
||||||
|
size_t n = words.size() - (words.empty() ? 0 : 1);
|
||||||
|
if (verb == "list" && n == 0) {
|
||||||
|
std::cout << boldblack << "Fonts\n" << black << describe_fonts();
|
||||||
|
} else if (verb == "samples" && n == 1) {
|
||||||
|
std::cout << "Font samples written to "
|
||||||
|
<< write_font_samples(words[1]) << "\n";
|
||||||
|
} else if (verb == "samples" && n == 2) {
|
||||||
|
std::cout << "Font samples written to "
|
||||||
|
<< write_font_samples(words[2], words[1]) << "\n";
|
||||||
|
} else if (verb == "install" && (n == 1 || n == 2)) {
|
||||||
|
std::cout << "Installing fonts from " << words[1] << ":\n"
|
||||||
|
<< install_fonts(words[1], n == 2 ? words[2] : "");
|
||||||
|
} else if (verb == "help") {
|
||||||
|
font_usage();
|
||||||
|
} else {
|
||||||
|
std::cout << "Unrecognized font command: --font " << join(words, " ")
|
||||||
|
<< "\n\n";
|
||||||
|
font_usage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int main(int argc, char* argv[])
|
int main(int argc, char* argv[])
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
@@ -22,6 +69,7 @@ int main(int argc, char* argv[])
|
|||||||
args.opt("input", "Input filename", "filename", "", "'text'");
|
args.opt("input", "Input filename", "filename", "", "'text'");
|
||||||
args.flag("targets", "Show targets defined by the input file");
|
args.flag("targets", "Show targets defined by the input file");
|
||||||
args.flag("klammers", "Show klammers defined by the input file");
|
args.flag("klammers", "Show klammers defined by the input file");
|
||||||
|
args.var("font", "List installed fonts. Enter \"--font help\" for font maintenance commands.");
|
||||||
args.opt("v", "'verbosity'", "n", "0", "'verbosity'");
|
args.opt("v", "'verbosity'", "n", "0", "'verbosity'");
|
||||||
|
|
||||||
if (show_usage(argc, argv)) {
|
if (show_usage(argc, argv)) {
|
||||||
@@ -55,6 +103,19 @@ int main(int argc, char* argv[])
|
|||||||
describe_rewrite_patterns();
|
describe_rewrite_patterns();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The --font subcommands operate on the Klammertext font store
|
||||||
|
// (infrastructure) and load no klammer set.
|
||||||
|
if (args.given("font")) {
|
||||||
|
strings_t words {};
|
||||||
|
for (const std::string& w : word_split(args.get("font"))) {
|
||||||
|
if (!w.empty()) {
|
||||||
|
words.push_back(w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
font_command(words);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
Machine M;
|
Machine M;
|
||||||
|
|
||||||
strings_t input_filenames = args.as_vector("input");
|
strings_t input_filenames = args.as_vector("input");
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ Klammertext's runtime environment is provided by a single self-configuring
|
|||||||
file. Source it from your shell profile (e.g., `~/.bashrc` or `~/.zshrc`):
|
file. Source it from your shell profile (e.g., `~/.bashrc` or `~/.zshrc`):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
source /path/to/klammertext/mac/env/runtime.env
|
source /path/to/klammertext/env/runtime.env
|
||||||
```
|
```
|
||||||
|
|
||||||
It self-locates `KLAMMERTEXT_HOME` from its own path, adds `bin/` and
|
It self-locates `KLAMMERTEXT_HOME` from its own path, adds `bin/` and
|
||||||
@@ -59,7 +59,7 @@ It self-locates `KLAMMERTEXT_HOME` from its own path, adds `bin/` and
|
|||||||
TeX Live is installed there), sets `LD_LIBRARY_PATH` so `libklammertext.so` is
|
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
|
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
|
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).
|
optional, gitignored `env/runtime.env.local` (sourced at the end).
|
||||||
|
|
||||||
After editing your shell profile, reload it:
|
After editing your shell profile, reload it:
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ source ~/.bashrc
|
|||||||
|
|
||||||
## Configure the build
|
## Configure the build
|
||||||
|
|
||||||
No build configuration is needed. The single `mac/env/makefile.env` is
|
No build configuration is needed. The single `env/makefile.env` is
|
||||||
cross-platform: it reads `KLAMMERTEXT_HOME` from the environment (set by
|
cross-platform: it reads `KLAMMERTEXT_HOME` from the environment (set by
|
||||||
`runtime.env` above), auto-detects the platform with `uname`, and auto-detects
|
`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
|
Python with `python3-config` — no hardcoded version and no per-host file to
|
||||||
@@ -149,7 +149,7 @@ 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`):
|
into it yourself (the package list is in `doc/install/texlive_additional_packages.sh`):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cat >> "$KLAMMERTEXT_HOME/mac/env/runtime.env.local" <<'EOF'
|
cat >> "$KLAMMERTEXT_HOME/env/runtime.env.local" <<'EOF'
|
||||||
export KLAMMERTEXT_TEXLIVE_BIN=/path/to/texlive/bin/x86_64-linux
|
export KLAMMERTEXT_TEXLIVE_BIN=/path/to/texlive/bin/x86_64-linux
|
||||||
export PATH="$KLAMMERTEXT_TEXLIVE_BIN:$PATH"
|
export PATH="$KLAMMERTEXT_TEXLIVE_BIN:$PATH"
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
Companion to `linux_source_install.md`. Verified on an Apple-Silicon Mac
|
Companion to `linux_source_install.md`. Verified on an Apple-Silicon Mac
|
||||||
(arm64, macOS 26 "Tahoe"). Klammertext's core (engine, SKS, HTML/LaTeX,
|
(arm64, macOS 26 "Tahoe"). Klammertext's core (engine, SKS, HTML/LaTeX,
|
||||||
`@image`) builds and runs natively with Apple Clang; the cross-platform build
|
`@image`) builds and runs natively with Apple Clang; the cross-platform build
|
||||||
environment (`mac/env/makefile.env`) auto-detects the OS via `uname`.
|
environment (`env/makefile.env`) auto-detects the OS via `uname`.
|
||||||
|
|
||||||
## 1. Toolchain prerequisites
|
## 1. Toolchain prerequisites
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ ffmpeg/openexr/etc.)
|
|||||||
```sh
|
```sh
|
||||||
git clone https://git.andykopra.com/ack/klammertext.git ~/projects/klammertext
|
git clone https://git.andykopra.com/ack/klammertext.git ~/projects/klammertext
|
||||||
# Set up the runtime environment (KLAMMERTEXT_HOME, PATH); add to ~/.zprofile:
|
# Set up the runtime environment (KLAMMERTEXT_HOME, PATH); add to ~/.zprofile:
|
||||||
echo 'source "$HOME/projects/klammertext/mac/env/runtime.env"' >> ~/.zprofile
|
echo 'source "$HOME/projects/klammertext/env/runtime.env"' >> ~/.zprofile
|
||||||
```
|
```
|
||||||
|
|
||||||
The single self-configuring `runtime.env` self-locates `KLAMMERTEXT_HOME` from
|
The single self-configuring `runtime.env` self-locates `KLAMMERTEXT_HOME` from
|
||||||
@@ -132,7 +132,7 @@ variable **and** prepend it to `PATH`, since `runtime.env.local` is sourced
|
|||||||
after the main `PATH` is built:
|
after the main `PATH` is built:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cat >> "$KLAMMERTEXT_HOME/mac/env/runtime.env.local" <<'EOF'
|
cat >> "$KLAMMERTEXT_HOME/env/runtime.env.local" <<'EOF'
|
||||||
export KLAMMERTEXT_TEXLIVE_BIN=/usr/local/texlive/2025basic/bin/universal-darwin
|
export KLAMMERTEXT_TEXLIVE_BIN=/usr/local/texlive/2025basic/bin/universal-darwin
|
||||||
export PATH="$KLAMMERTEXT_TEXLIVE_BIN:$PATH"
|
export PATH="$KLAMMERTEXT_TEXLIVE_BIN:$PATH"
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
0
mac/env/lsan.supp → env/lsan.supp
vendored
0
mac/env/lsan.supp → env/lsan.supp
vendored
8
mac/env/makefile.env → env/makefile.env
vendored
8
mac/env/makefile.env → env/makefile.env
vendored
@@ -1,7 +1,7 @@
|
|||||||
# mac/env/makefile.env — single cross-platform build environment for Klammertext.
|
# env/makefile.env — single cross-platform build environment for Klammertext.
|
||||||
#
|
#
|
||||||
# Included identically by every component Makefile:
|
# Included identically by every component Makefile:
|
||||||
# include $(KLAMMERTEXT_HOME)/mac/env/makefile.env
|
# include $(KLAMMERTEXT_HOME)/env/makefile.env
|
||||||
#
|
#
|
||||||
# The platform is auto-detected with uname; the compiler is chosen with
|
# The platform is auto-detected with uname; the compiler is chosen with
|
||||||
# make COMPILER=gcc (default)
|
# make COMPILER=gcc (default)
|
||||||
@@ -9,10 +9,10 @@
|
|||||||
# There is no per-host/per-OS file and no DESKTOP_SESSION/HOST/SITE selector.
|
# There is no per-host/per-OS file and no DESKTOP_SESSION/HOST/SITE selector.
|
||||||
|
|
||||||
ifndef KLAMMERTEXT_HOME
|
ifndef KLAMMERTEXT_HOME
|
||||||
$(error KLAMMERTEXT_HOME is not set -- source mac/env/runtime.env first)
|
$(error KLAMMERTEXT_HOME is not set -- source env/runtime.env first)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
include $(KLAMMERTEXT_HOME)/mac/env/optimize.env
|
include $(KLAMMERTEXT_HOME)/env/optimize.env
|
||||||
|
|
||||||
UNAME_S := $(shell uname -s)
|
UNAME_S := $(shell uname -s)
|
||||||
CPP_VERSION := c++20
|
CPP_VERSION := c++20
|
||||||
0
mac/env/optimize.env → env/optimize.env
vendored
0
mac/env/optimize.env → env/optimize.env
vendored
18
mac/env/runtime.env → env/runtime.env
vendored
18
mac/env/runtime.env → env/runtime.env
vendored
@@ -1,22 +1,22 @@
|
|||||||
# mac/env/runtime.env — single self-configuring runtime environment for Klammertext.
|
# env/runtime.env — single self-configuring runtime environment for Klammertext.
|
||||||
#
|
#
|
||||||
# Source this from your shell profile (~/.bashrc etc.):
|
# Source this from your shell profile (~/.bashrc etc.):
|
||||||
# source /path/to/klammertext/K/mac/env/runtime.env
|
# source /path/to/klammertext/K/env/runtime.env
|
||||||
#
|
#
|
||||||
# This is the runtime counterpart of the shared mac/env/makefile.env: one file
|
# This is the runtime counterpart of the shared env/makefile.env: one file
|
||||||
# for every machine, with all machine-specific values AUTO-DETECTED. There is no
|
# for every machine, with all machine-specific values AUTO-DETECTED. There is no
|
||||||
# per-host runtime.env.<host> file and no HOST/OS/SITE selector.
|
# per-host runtime.env.<host> file and no HOST/OS/SITE selector.
|
||||||
# - KLAMMERTEXT_HOME : derived from this file's own location (self-locating)
|
# - KLAMMERTEXT_HOME : derived from this file's own location (self-locating)
|
||||||
# - KLAMMERTEXT_TEXLIVE_BIN : newest ~/external/texlive/<year>/bin/<arch>
|
# - KLAMMERTEXT_TEXLIVE_BIN : newest ~/external/texlive/<year>/bin/<arch>
|
||||||
# Machine-unique, non-committable additions (extra library paths such as NVIDIA
|
# 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
|
# iray, local tools, etc.) go in an optional, gitignored env/runtime.env.local
|
||||||
# sourced at the end -- NOT in this shared file, so one machine's bundled
|
# sourced at the end -- NOT in this shared file, so one machine's bundled
|
||||||
# libraries can't shadow another's system libraries.
|
# libraries can't shadow another's system libraries.
|
||||||
|
|
||||||
# --- KLAMMERTEXT_HOME: self-locate (bash sets BASH_SOURCE; zsh sets $0) --------
|
# --- 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.
|
# This file lives at $KLAMMERTEXT_HOME/env/runtime.env, so go up one level.
|
||||||
_kt_self="${BASH_SOURCE[0]:-$0}"
|
_kt_self="${BASH_SOURCE[0]:-$0}"
|
||||||
export KLAMMERTEXT_HOME="$(cd "$(dirname "$_kt_self")/../.." && pwd)"
|
export KLAMMERTEXT_HOME="$(cd "$(dirname "$_kt_self")/.." && pwd)"
|
||||||
unset _kt_self
|
unset _kt_self
|
||||||
|
|
||||||
_kt_uname="$(uname -s)"
|
_kt_uname="$(uname -s)"
|
||||||
@@ -58,7 +58,7 @@ else
|
|||||||
# LSan suppressions for unactionable libpython/OpenImageIO leaks (see
|
# LSan suppressions for unactionable libpython/OpenImageIO leaks (see
|
||||||
# lsan.supp). Real leaks in Klammertext code are still reported; no effect
|
# lsan.supp). Real leaks in Klammertext code are still reported; no effect
|
||||||
# in performance builds (OPTIMIZE=1, which disables ASan).
|
# in performance builds (OPTIMIZE=1, which disables ASan).
|
||||||
export LSAN_OPTIONS="suppressions=$KLAMMERTEXT_HOME/mac/env/lsan.supp:print_suppressions=0"
|
export LSAN_OPTIONS="suppressions=$KLAMMERTEXT_HOME/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}"
|
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
|
fi
|
||||||
unset _kt_uname
|
unset _kt_uname
|
||||||
@@ -67,6 +67,6 @@ unset _kt_uname
|
|||||||
# Use an if-block (not `[ -f ] && .`) so that when the local file is absent this
|
# 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
|
# file's final exit status is 0 -- otherwise `source runtime.env` returns
|
||||||
# non-zero, breaking `source runtime.env && ...` and `set -e` callers.
|
# non-zero, breaking `source runtime.env && ...` and `set -e` callers.
|
||||||
if [ -f "$KLAMMERTEXT_HOME/mac/env/runtime.env.local" ]; then
|
if [ -f "$KLAMMERTEXT_HOME/env/runtime.env.local" ]; then
|
||||||
. "$KLAMMERTEXT_HOME/mac/env/runtime.env.local"
|
. "$KLAMMERTEXT_HOME/env/runtime.env.local"
|
||||||
fi
|
fi
|
||||||
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
@@ -4,13 +4,13 @@
|
|||||||
K := $(KLAMMERTEXT_HOME)
|
K := $(KLAMMERTEXT_HOME)
|
||||||
KS := $(K)/sks
|
KS := $(K)/sks
|
||||||
|
|
||||||
include $(K)/mac/env/makefile.env
|
include $(K)/env/makefile.env
|
||||||
|
|
||||||
# Source files
|
# Source files
|
||||||
BASENAMES := util error locator file argv character ktype katom katom_list \
|
BASENAMES := util error locator file argv character ktype katom katom_list \
|
||||||
log show command argument argument_set argtype argtype_set \
|
log show command argument argument_set argtype argtype_set \
|
||||||
state eval eval_python eval_cpp klammer klammer_set deftype \
|
state eval eval_python eval_cpp klammer klammer_set deftype \
|
||||||
target target_set machine
|
target target_set machine font_store
|
||||||
|
|
||||||
SOURCES := $(addsuffix .cpp,$(BASENAMES))
|
SOURCES := $(addsuffix .cpp,$(BASENAMES))
|
||||||
OBJECTS := $(addsuffix .o,$(BASENAMES))
|
OBJECTS := $(addsuffix .o,$(BASENAMES))
|
||||||
|
|||||||
@@ -7,12 +7,13 @@
|
|||||||
#include "util.h"
|
#include "util.h"
|
||||||
|
|
||||||
Argtype::Argtype(std::string name, std::string desc, std::string symbolic_pattern, std::string pattern,
|
Argtype::Argtype(std::string name, std::string desc, std::string symbolic_pattern, std::string pattern,
|
||||||
std::string python_cast, modify_string_f python_format,
|
std::string default_value, std::string python_cast, modify_string_f python_format,
|
||||||
const Locator& loc)
|
const Locator& loc)
|
||||||
: m_name(name)
|
: m_name(name)
|
||||||
, m_desc(desc)
|
, m_desc(desc)
|
||||||
, m_symbolic_pattern(symbolic_pattern)
|
, m_symbolic_pattern(symbolic_pattern)
|
||||||
, m_pattern(pattern)
|
, m_pattern(pattern)
|
||||||
|
, m_default(default_value)
|
||||||
, m_python_cast(python_cast)
|
, m_python_cast(python_cast)
|
||||||
, m_python_format(python_format)
|
, m_python_format(python_format)
|
||||||
, m_regex(std::regex(pattern))
|
, m_regex(std::regex(pattern))
|
||||||
@@ -20,22 +21,25 @@ Argtype::Argtype(std::string name, std::string desc, std::string symbolic_patter
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool empty_value(const strings_t& v)
|
||||||
|
{
|
||||||
|
return v.empty() || (v.size() == 1 && v[0].empty());
|
||||||
|
}
|
||||||
|
|
||||||
std::string pyformat_string(const strings_t& v)
|
std::string pyformat_string(const strings_t& v)
|
||||||
{
|
{
|
||||||
std::string p = v[0];
|
std::string p = v.empty() ? "" : v[0];
|
||||||
std::string delim = "\"";
|
p = string_replace(p, "\\", "\\\\");
|
||||||
if (contains(p, "\"\"\"")) {
|
p = string_replace(p, "\"", "\\\"");
|
||||||
delim = "'''";
|
p = string_replace(p, "\n", "\\n");
|
||||||
} else if (contains(p, "'''")) {
|
return "\"" + p + "\"";
|
||||||
delim = "\"\"\"";
|
|
||||||
} else if (contains(p, "\"")) {
|
|
||||||
delim = "'";
|
|
||||||
}
|
|
||||||
return delim + p + delim;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string pyformat_bool(const strings_t& v)
|
std::string pyformat_bool(const strings_t& v)
|
||||||
{
|
{
|
||||||
|
if (empty_value(v)) {
|
||||||
|
return "None";
|
||||||
|
}
|
||||||
std::string value = v[0];
|
std::string value = v[0];
|
||||||
std::set<std::string> true_values { "1", "true", "True", "yes" };
|
std::set<std::string> true_values { "1", "true", "True", "yes" };
|
||||||
std::set<std::string> false_values { "0", "false", "False", "no" };
|
std::set<std::string> false_values { "0", "false", "False", "no" };
|
||||||
@@ -50,6 +54,9 @@ std::string pyformat_bool(const strings_t& v)
|
|||||||
|
|
||||||
std::string pyformat_number(const strings_t& value)
|
std::string pyformat_number(const strings_t& value)
|
||||||
{
|
{
|
||||||
|
if (empty_value(value)) {
|
||||||
|
return "None";
|
||||||
|
}
|
||||||
return value[0];
|
return value[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,12 +69,29 @@ std::string pylist(strings_t words)
|
|||||||
|
|
||||||
std::string pyformat_list(const strings_t& value)
|
std::string pyformat_list(const strings_t& value)
|
||||||
{
|
{
|
||||||
return pylist(value);
|
strings_t words {};
|
||||||
|
for (const std::string& v : value) {
|
||||||
|
for (const std::string& word : word_split(v)) {
|
||||||
|
if (!word.empty()) {
|
||||||
|
words.push_back(word);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pylist(words);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string pyformat_dlist(const strings_t& value)
|
std::string pyformat_dlist(const strings_t& value)
|
||||||
{
|
{
|
||||||
return pylist(value);
|
strings_t items {};
|
||||||
|
for (const std::string& v : value) {
|
||||||
|
if (v.empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const std::string& item : dlist_split(v)) {
|
||||||
|
items.push_back(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pylist(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Argtype::python_value(const std::string& var_name, std::vector<std::string> value, size_t name_size)
|
std::string Argtype::python_value(const std::string& var_name, std::vector<std::string> value, size_t name_size)
|
||||||
@@ -76,7 +100,18 @@ std::string Argtype::python_value(const std::string& var_name, std::vector<std::
|
|||||||
ss << " " << std::left << std::setw(name_size) << var_name << " = ";
|
ss << " " << std::left << std::setw(name_size) << var_name << " = ";
|
||||||
if (m_python_format) {
|
if (m_python_format) {
|
||||||
ss << m_python_format(value);
|
ss << m_python_format(value);
|
||||||
|
} else if (m_python_cast.empty() || m_python_cast == "str") {
|
||||||
|
// String-family types (and untyped variables): plain quoted string.
|
||||||
|
ss << pyformat_string(value);
|
||||||
|
} else if (!m_parameter.empty()) {
|
||||||
|
// Parameterized type: bind the type parameter as N around the
|
||||||
|
// cast, e.g. (lambda N: <cast>)(2)("A | B || C | D").
|
||||||
|
ss << "(lambda N: " << m_python_cast << ")(" << m_parameter << ")("
|
||||||
|
<< pyformat_string(value) << ")";
|
||||||
} else {
|
} else {
|
||||||
|
// User-defined :python_cast expression, applied to the raw value.
|
||||||
|
// The cast is applied to an empty value too, so e.g. a split lambda
|
||||||
|
// yields [] for an unsupplied argument.
|
||||||
ss << m_python_cast << "(" << pyformat_string(value) << ")";
|
ss << m_python_cast << "(" << pyformat_string(value) << ")";
|
||||||
}
|
}
|
||||||
return ss.str();
|
return ss.str();
|
||||||
|
|||||||
@@ -17,23 +17,42 @@ public:
|
|||||||
Argtype()
|
Argtype()
|
||||||
: m_name("default")
|
: m_name("default")
|
||||||
, m_desc("default argument type")
|
, m_desc("default argument type")
|
||||||
, m_symbolic_pattern(".+")
|
, m_symbolic_pattern(R"((?:.|\n)*)")
|
||||||
, m_pattern(".+")
|
, m_pattern(R"((?:.|\n)*)")
|
||||||
, m_python_cast("str")
|
, m_python_cast("str")
|
||||||
, m_python_format()
|
, m_python_format()
|
||||||
|
, m_regex(std::regex(R"((?:.|\n)*)"))
|
||||||
, m_loc()
|
, m_loc()
|
||||||
{};
|
{};
|
||||||
|
|
||||||
Argtype(std::string name, std::string desc, std::string symbolic_pattern, std::string pattern,
|
Argtype(std::string name, std::string desc, std::string symbolic_pattern, std::string pattern,
|
||||||
std::string python_cast, modify_string_f python_format,
|
std::string default_value, std::string python_cast, modify_string_f python_format,
|
||||||
const Locator& loc);
|
const Locator& loc);
|
||||||
|
|
||||||
std::string python_value(const std::string& var_name, std::vector<std::string> value, size_t name_size);
|
std::string python_value(const std::string& var_name, std::vector<std::string> value, size_t name_size);
|
||||||
|
|
||||||
|
// True for the match-everything pattern shared by the string family
|
||||||
|
// (string, rest, literal, and the default type). Validating against
|
||||||
|
// it is pointless, and running std::regex over a large value (e.g.
|
||||||
|
// @document's :text holding a whole document) overflows the regex
|
||||||
|
// executor's recursion stack.
|
||||||
|
bool matches_all() const { return m_pattern == R"((?:.|\n)*)"; };
|
||||||
|
|
||||||
std::string m_name {};
|
std::string m_name {};
|
||||||
std::string m_desc {};
|
std::string m_desc {};
|
||||||
std::string m_symbolic_pattern {};
|
std::string m_symbolic_pattern {};
|
||||||
std::string m_pattern {};
|
std::string m_pattern {};
|
||||||
|
// Default value for parameters of this type; a default given in a
|
||||||
|
// klammer's parameter declaration overrides it (see
|
||||||
|
// parse_optional_parameter). Useful for single-purpose types
|
||||||
|
// (cell_hpos, column_width); general types (bool, float) have no
|
||||||
|
// sensible universal default and leave it empty.
|
||||||
|
std::string m_default {};
|
||||||
|
// Type parameter for parameterized types like rest(2): the value N
|
||||||
|
// is bound around the python cast as (lambda N: <cast>)(2)(...).
|
||||||
|
// Empty means unparameterized; a type with a default parameter
|
||||||
|
// (rest -> "1") is specialized by writing type(N) in a declaration.
|
||||||
|
std::string m_parameter {};
|
||||||
std::string m_python_cast {};
|
std::string m_python_cast {};
|
||||||
modify_string_f m_python_format {};
|
modify_string_f m_python_format {};
|
||||||
std::regex m_regex {};
|
std::regex m_regex {};
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
Parameter_set& Argtype_set::parameters()
|
Parameter_set& Argtype_set::parameters()
|
||||||
{
|
{
|
||||||
static Parameter_set instance("name | desc :pattern .* :python_cast str");
|
static Parameter_set instance("name | desc :pattern .* :python_cast str :default");
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,8 +22,11 @@ Argtype_set::Argtype_set()
|
|||||||
(void)(void)K::log(2);
|
(void)(void)K::log(2);
|
||||||
Locator loc = current_locator();
|
Locator loc = current_locator();
|
||||||
for (auto [name, desc, pattern, python_cast, python_format] : base_argtypes) {
|
for (auto [name, desc, pattern, python_cast, python_format] : base_argtypes) {
|
||||||
add(name, desc, pattern, python_cast, python_format, loc);
|
add(name, desc, pattern, "", python_cast, python_format, loc);
|
||||||
}
|
}
|
||||||
|
// rest is a parameterized type (rest(N)); an unparameterized use is
|
||||||
|
// one-dimensional.
|
||||||
|
m_types["rest"].m_parameter = "1";
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Argtype_set::replace_symbols(const std::string& pattern, const Locator& loc)
|
std::string Argtype_set::replace_symbols(const std::string& pattern, const Locator& loc)
|
||||||
@@ -59,7 +62,7 @@ void Argtype_set::check_for_existing_definition(
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Argtype_set::add(const std::string& name, const std::string& desc,
|
void Argtype_set::add(const std::string& name, const std::string& desc,
|
||||||
const std::string& pattern,
|
const std::string& pattern, const std::string& default_value,
|
||||||
const std::string& python_cast, modify_string_f python_format,
|
const std::string& python_cast, modify_string_f python_format,
|
||||||
const Locator& loc)
|
const Locator& loc)
|
||||||
{
|
{
|
||||||
@@ -68,17 +71,35 @@ void Argtype_set::add(const std::string& name, const std::string& desc,
|
|||||||
std::string expanded_pattern = replace_symbols(pattern, loc);
|
std::string expanded_pattern = replace_symbols(pattern, loc);
|
||||||
m_name_size = std::max(m_name_size, name.size()); // For display
|
m_name_size = std::max(m_name_size, name.size()); // For display
|
||||||
m_pattern_size = std::max(m_pattern_size, expanded_pattern.size());
|
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);
|
try {
|
||||||
|
m_types[name] = Argtype(name, desc, pattern, expanded_pattern,
|
||||||
|
default_value, python_cast, python_format, loc);
|
||||||
|
} catch (const std::regex_error& e) {
|
||||||
|
std::stringstream ss {};
|
||||||
|
ss << "The pattern for argument type \"" << name
|
||||||
|
<< "\" is not a valid regular expression (" << e.what() << "):\n"
|
||||||
|
<< " " << pattern << "\n";
|
||||||
|
if (pattern != expanded_pattern) {
|
||||||
|
ss << "expanded to:\n " << expanded_pattern << "\n";
|
||||||
|
}
|
||||||
|
throw Definition_error(ss.str(), loc, false);
|
||||||
|
}
|
||||||
|
if (!default_value.empty() &&
|
||||||
|
!std::regex_match(default_value, m_types[name].m_regex)) {
|
||||||
|
std::stringstream ss {};
|
||||||
|
ss << "The default value \"" << default_value << "\" for argument type \""
|
||||||
|
<< name << "\" does not match its own pattern:\n"
|
||||||
|
<< " " << pattern << "\n";
|
||||||
|
throw Definition_error(ss.str(), loc, false);
|
||||||
|
}
|
||||||
m_names.push_back(name);
|
m_names.push_back(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Argtype_set::add(std::vector<Katom>::iterator begin, std::vector<Katom>::iterator end, std::vector<Katom>& katoms)
|
void Argtype_set::add(std::vector<Katom>::iterator begin, std::vector<Katom>::iterator end, std::vector<Katom>& katoms)
|
||||||
{
|
{
|
||||||
(void)K::log(3);
|
(void)K::log(3);
|
||||||
//Argument_set parameters("name | desc :pattern .* :python_cast str");
|
|
||||||
|
|
||||||
auto [positional, optional, rest] =
|
auto [positional, optional, rest] =
|
||||||
argument_split(begin + 1, end); //, Argtype_set::parameters.m_positional.size());
|
argument_split(begin + 1, end - 1); //, Argtype_set::parameters.m_positional.size());
|
||||||
|
|
||||||
check_for_existing_definition(positional[0][0].m_text, begin->m_loc);
|
check_for_existing_definition(positional[0][0].m_text, begin->m_loc);
|
||||||
|
|
||||||
@@ -86,11 +107,8 @@ void Argtype_set::add(std::vector<Katom>::iterator begin, std::vector<Katom>::it
|
|||||||
|
|
||||||
// std::for_each(begin, end+1, [](Katom& k) { k.m_type = katom_t::replaced; });
|
// std::for_each(begin, end+1, [](Katom& k) { k.m_type = katom_t::replaced; });
|
||||||
|
|
||||||
modify_string_f pyformat {};
|
add(values["name"], values["desc"], values["pattern"], values["default"],
|
||||||
std::string pycast {};
|
values["python_cast"], modify_string_f{},
|
||||||
|
|
||||||
add(values["name"], values["desc"], values["pattern"],
|
|
||||||
pycast, pyformat,
|
|
||||||
begin->m_loc);
|
begin->m_loc);
|
||||||
|
|
||||||
modify_type(katom_t::replaced, begin, end);
|
modify_type(katom_t::replaced, begin, end);
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ public:
|
|||||||
std::string replace_symbols(const std::string& pattern, const Locator& loc);
|
std::string replace_symbols(const std::string& pattern, const Locator& loc);
|
||||||
void check_for_existing_definition(const std::string& name, 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,
|
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 std::string& pattern, const std::string& default_value,
|
||||||
|
const std::string& python_cast, modify_string_f python_format,
|
||||||
const Locator& loc);
|
const Locator& loc);
|
||||||
void add(std::vector<Katom>::iterator begin, std::vector<Katom>::iterator end, std::vector<Katom>& katoms);
|
void add(std::vector<Katom>::iterator begin, std::vector<Katom>::iterator end, std::vector<Katom>& katoms);
|
||||||
|
|
||||||
@@ -63,7 +64,7 @@ std::vector<std::tuple<std::string, std::string, std::string, std::string, modif
|
|||||||
pyformat_number },
|
pyformat_number },
|
||||||
|
|
||||||
{ "float", "a floating-point number",
|
{ "float", "a floating-point number",
|
||||||
R"([-+]?\d+(\.\d*)?)", "float",
|
R"([-+]?(\d+(\.\d*)?|\.\d+))", "float",
|
||||||
pyformat_number },
|
pyformat_number },
|
||||||
|
|
||||||
{ "fraction", "a number in the form 'n/d'",
|
{ "fraction", "a number in the form 'n/d'",
|
||||||
@@ -78,9 +79,11 @@ std::vector<std::tuple<std::string, std::string, std::string, std::string, modif
|
|||||||
R"((?:.|\n)*)", "",
|
R"((?:.|\n)*)", "",
|
||||||
pyformat_dlist },
|
pyformat_dlist },
|
||||||
|
|
||||||
{ "rest", "a list of strings delimited by the bar character",
|
{ "rest", "the remaining arguments as a list nested to N dimensions "
|
||||||
R"(.*)", "str",
|
"(rest(N)); the delimiter for dimension n is a run of n bar "
|
||||||
pyformat_string },
|
"characters, so | separates elements and || lists of elements",
|
||||||
|
R"((?:.|\n)*)", R"((lambda s : __import__("kutil").rest_split(s, N)))",
|
||||||
|
nullptr },
|
||||||
|
|
||||||
{ "literal", "literal text passed without interpretation",
|
{ "literal", "literal text passed without interpretation",
|
||||||
R"((?:.|\n)*)", "str",
|
R"((?:.|\n)*)", "str",
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ public:
|
|||||||
Argtype m_argtype; // {};
|
Argtype m_argtype; // {};
|
||||||
bool m_optional {};
|
bool m_optional {};
|
||||||
std::string m_default {};
|
std::string m_default {};
|
||||||
|
// True when m_default was filled from the argument type's :default
|
||||||
|
// rather than declared in the klammer's parameter list (for kdesc
|
||||||
|
// provenance display).
|
||||||
|
bool m_default_from_type {};
|
||||||
Locator m_loc;
|
Locator m_loc;
|
||||||
std::string m_target {};
|
std::string m_target {};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,16 +17,44 @@ bool operator==(Parameter_set lhs, Parameter_set rhs)
|
|||||||
|
|
||||||
std::regex parameter_regex(bool optional=false)
|
std::regex parameter_regex(bool optional=false)
|
||||||
{
|
{
|
||||||
//std::string pattern = R"(([A-Za-z]\w*)(?:(\.\w*)(?:(\.\w*)))?)";
|
// The type component may carry a numeric type parameter, e.g.
|
||||||
std::string pattern = R"((?:([A-Za-z]\w*))|(?:([A-Za-z]\w*)\.(\w+))|(?:([A-Za-z]\w*)\.(\w+)\.(\w+)))";
|
// rows.rest(2) (see resolve_argtype below). In the optional
|
||||||
|
// three-part form the type may be empty (:paper_size..tex).
|
||||||
|
std::string type = R"(\w+(?:\(\d+\))?)";
|
||||||
|
std::string opt_type = R"(\w*(?:\(\d+\))?)";
|
||||||
|
std::string pattern = R"((?:([A-Za-z]\w*))|(?:([A-Za-z]\w*)\.()" + type +
|
||||||
|
R"())|(?:([A-Za-z]\w*)\.()" + type + R"()\.(\w+)))";
|
||||||
if (optional) {
|
if (optional) {
|
||||||
pattern = R"((?::([A-Za-z]\w*))|(?::([A-Za-z]\w*)\.(\w+))|(?::([A-Za-z]\w*)\.(\w*)\.(\w+)))";
|
pattern = R"((?::([A-Za-z]\w*))|(?::([A-Za-z]\w*)\.()" + type +
|
||||||
// x x
|
R"())|(?::([A-Za-z]\w*)\.()" + opt_type + R"()\.(\w+)))";
|
||||||
}
|
}
|
||||||
// (void)K::log(3, pattern);
|
// (void)K::log(3, pattern);
|
||||||
return std::regex(pattern);
|
return std::regex(pattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Look up an argument type, specializing a parameterized use such as
|
||||||
|
// rest(2): the base type is copied, its type parameter set, and its
|
||||||
|
// display name extended, so kdesc signatures show rows.rest(2).
|
||||||
|
static Argtype resolve_argtype(
|
||||||
|
const std::string& type_text, const Argtype_set& argtypes, const Locator& loc)
|
||||||
|
{
|
||||||
|
static const std::regex parameterized(R"((\w+)\((\d+)\))");
|
||||||
|
std::smatch match {};
|
||||||
|
if (std::regex_match(type_text, match, parameterized)) {
|
||||||
|
Argtype argtype = argtypes.get(match[1], loc);
|
||||||
|
argtype.m_parameter = match[2];
|
||||||
|
argtype.m_name += "(" + std::string(match[2]) + ")";
|
||||||
|
return argtype;
|
||||||
|
}
|
||||||
|
return argtypes.get(type_text, loc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// True for the rest type in any parameterization (rest, rest(2), ...).
|
||||||
|
static bool is_rest(const Argtype& argtype)
|
||||||
|
{
|
||||||
|
return argtype.m_name == "rest" || argtype.m_name.rfind("rest(", 0) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
Parameter_set::Parameter_set(const std::string parameter_string)
|
Parameter_set::Parameter_set(const std::string parameter_string)
|
||||||
{
|
{
|
||||||
(void)K::log(3);
|
(void)K::log(3);
|
||||||
@@ -77,7 +105,7 @@ Parameter parse_positional_parameter(const katom_list& katoms, const Argtype_set
|
|||||||
if (match_type.empty()) {
|
if (match_type.empty()) {
|
||||||
match_type = "string";
|
match_type = "string";
|
||||||
}
|
}
|
||||||
return Parameter(match_name, argtypes.get(match_type, k.m_loc), k.m_loc);
|
return Parameter(match_name, resolve_argtype(match_type, argtypes, k.m_loc), k.m_loc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,8 +133,19 @@ Parameter parse_optional_parameter(const katom_list& katoms, const Argtype_set&
|
|||||||
if (match_type.empty()) {
|
if (match_type.empty()) {
|
||||||
match_type = "string";
|
match_type = "string";
|
||||||
}
|
}
|
||||||
return Parameter(match_name, argtypes.get(match_type, k.m_loc),
|
Parameter parameter(match_name, resolve_argtype(match_type, argtypes, k.m_loc),
|
||||||
k.m_loc, true, default_value);
|
k.m_loc, true, default_value);
|
||||||
|
// Two-level default resolution: a default declared in the
|
||||||
|
// parameter list wins; otherwise the argument type's :default
|
||||||
|
// fills in. Both are validated here, at definition time, so an
|
||||||
|
// invalid default cannot reach an application.
|
||||||
|
if (default_value.empty() && !parameter.m_argtype.m_default.empty()) {
|
||||||
|
parameter.m_default = parameter.m_argtype.m_default;
|
||||||
|
parameter.m_default_from_type = true;
|
||||||
|
} else if (!default_value.empty()) {
|
||||||
|
Parameter_set::validate(parameter, default_value, k.m_loc);
|
||||||
|
}
|
||||||
|
return parameter;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,7 +252,7 @@ void Parameter_set::parse_parameters(const katom_list& katoms, const Argtype_set
|
|||||||
auto [positional, optional] = parameter_split(katoms.cbegin(), katoms.cend());
|
auto [positional, optional] = parameter_split(katoms.cbegin(), katoms.cend());
|
||||||
for (auto req : positional) {
|
for (auto req : positional) {
|
||||||
auto pos = parse_positional_parameter(req, argtypes);
|
auto pos = parse_positional_parameter(req, argtypes);
|
||||||
if (pos.m_argtype.m_name == "rest") {
|
if (is_rest(pos.m_argtype)) {
|
||||||
m_rest.push_back(pos);
|
m_rest.push_back(pos);
|
||||||
} else {
|
} else {
|
||||||
m_positional.push_back(pos);
|
m_positional.push_back(pos);
|
||||||
@@ -295,6 +334,13 @@ argument_split(katom_list::const_iterator kbegin, katom_list::const_iterator ken
|
|||||||
} else if (positional.size() < positional_limit) {
|
} else if (positional.size() < positional_limit) {
|
||||||
positional.push_back(trim_part(p));
|
positional.push_back(trim_part(p));
|
||||||
} else {
|
} else {
|
||||||
|
// Parts were trimmed, so adjacent parts would abut their bar
|
||||||
|
// katoms (a row separator "||" next to an empty cell's "|"
|
||||||
|
// would serialize as "|||"). A space keeps the writer's
|
||||||
|
// bar/double-bar distinction parseable.
|
||||||
|
if (!rest.empty()) {
|
||||||
|
rest.push_back(Katom(" ", katom_t::space, p[0].m_loc));
|
||||||
|
}
|
||||||
rest.insert(rest.end(), p.begin(), p.end());
|
rest.insert(rest.end(), p.begin(), p.end());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -386,9 +432,59 @@ Parameter_set::value_map(
|
|||||||
throw Argument_error(ss.str(), loc);
|
throw Argument_error(ss.str(), loc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (const auto& [name, value] : values) {
|
||||||
|
const Parameter* parameter = find(name);
|
||||||
|
if (parameter) {
|
||||||
|
validate(*parameter, value, loc);
|
||||||
|
}
|
||||||
|
}
|
||||||
return values;
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check an argument value against its argument type's pattern. An empty
|
||||||
|
// value (an unsupplied optional argument without a default) is not checked.
|
||||||
|
// The error message includes the argument type's description from its
|
||||||
|
// @@@argtype definition, so the .k description text is what the writer
|
||||||
|
// sees when a complicated value (e.g. a table line specification) is wrong.
|
||||||
|
// The value-size limit guards against std::regex stack overflow: the
|
||||||
|
// libstdc++ executor recurses per character, so a pattern applied to a
|
||||||
|
// very large value crashes. Typed argument values are short; large
|
||||||
|
// values are content (rest, :text) whose types match everything and are
|
||||||
|
// excluded by matches_all() anyway.
|
||||||
|
const size_t validation_size_limit = 4096;
|
||||||
|
|
||||||
|
void Parameter_set::validate(
|
||||||
|
const Parameter& parameter, const std::string& value, const Locator& loc)
|
||||||
|
{
|
||||||
|
if (value.empty() || value.size() > validation_size_limit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const Argtype& argtype = parameter.m_argtype;
|
||||||
|
if (argtype.matches_all()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!std::regex_match(value, argtype.m_regex)) {
|
||||||
|
std::stringstream ss {};
|
||||||
|
ss << "The value \"" << value << "\" given for the argument \""
|
||||||
|
<< parameter.m_name << "\" does not match the \"" << argtype.m_name
|
||||||
|
<< "\" argument type:\n\n"
|
||||||
|
<< trim(argtype.m_desc) << "\n";
|
||||||
|
throw Argument_error(ss.str(), loc, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Parameter* Parameter_set::find(const std::string& name) const
|
||||||
|
{
|
||||||
|
for (const auto& params : {&m_positional, &m_optional, &m_rest}) {
|
||||||
|
for (const Parameter& p : *params) {
|
||||||
|
if (p.m_name == name) {
|
||||||
|
return &p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
// Parameter/argument substitution
|
// Parameter/argument substitution
|
||||||
|
|
||||||
std::string replace_arguments(
|
std::string replace_arguments(
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ public:
|
|||||||
const std::vector<std::vector<Katom>>& optional,
|
const std::vector<std::vector<Katom>>& optional,
|
||||||
const std::vector<Katom>& rest,
|
const std::vector<Katom>& rest,
|
||||||
const Locator& loc);
|
const Locator& loc);
|
||||||
|
const Parameter* find(const std::string& name) const;
|
||||||
|
static void validate(
|
||||||
|
const Parameter& parameter, const std::string& value, const Locator& loc);
|
||||||
bool empty() const { return m_katoms.size() == 0; };
|
bool empty() const { return m_katoms.size() == 0; };
|
||||||
|
|
||||||
std::vector<Katom> m_katoms {};
|
std::vector<Katom> m_katoms {};
|
||||||
|
|||||||
35
mac/argv.cpp
35
mac/argv.cpp
@@ -145,6 +145,40 @@ void Argv::opt(const std::string& name, const std::string& desc, const std::stri
|
|||||||
update_width(arg);
|
update_width(arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Argv::var(const std::string& name, const std::string& desc)
|
||||||
|
{
|
||||||
|
(void)K::log(2, name, desc);
|
||||||
|
Arg arg {};
|
||||||
|
arg.m_type = "var";
|
||||||
|
arg.m_name = name;
|
||||||
|
arg.m_desc = get_regex_desc(desc);
|
||||||
|
arg.m_syntax = arg.symbol();
|
||||||
|
m_args[name] = arg;
|
||||||
|
m_names.push_back(name);
|
||||||
|
m_var_names.push_back(name);
|
||||||
|
m_hyphen_markers.push_back(flag_name(name));
|
||||||
|
update_width(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Argv::parse_vars(strings_t& words, string_map& named_args)
|
||||||
|
{
|
||||||
|
for (const std::string& name : m_var_names) {
|
||||||
|
auto it = std::ranges::find(words, flag_name(name));
|
||||||
|
named_args[name] = "";
|
||||||
|
if (it == words.end()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
m_given.insert(name);
|
||||||
|
auto first = it + 1;
|
||||||
|
auto last = first;
|
||||||
|
while (last != words.end() && !(*last).empty() && (*last)[0] != '-') {
|
||||||
|
last++;
|
||||||
|
}
|
||||||
|
named_args[name] = join(strings_t(first, last), " ");
|
||||||
|
words.erase(it, last);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Argv::usage_line(Arg arg)
|
void Argv::usage_line(Arg arg)
|
||||||
{
|
{
|
||||||
std::cout.fill(' ');
|
std::cout.fill(' ');
|
||||||
@@ -330,6 +364,7 @@ Argv::classify_arguments(int argc, char* argv[], bool full_parse)
|
|||||||
if (full_parse) {
|
if (full_parse) {
|
||||||
check_flags_and_options(argv[0], words);
|
check_flags_and_options(argv[0], words);
|
||||||
}
|
}
|
||||||
|
parse_vars(words, named_args);
|
||||||
parse_flags(words, named_args);
|
parse_flags(words, named_args);
|
||||||
parse_optional(words, named_args);
|
parse_optional(words, named_args);
|
||||||
parse_positional(argv_to_string(argc, argv), join(words, " "), named_args);
|
parse_positional(argv_to_string(argc, argv), join(words, " "), named_args);
|
||||||
|
|||||||
14
mac/argv.h
14
mac/argv.h
@@ -3,6 +3,7 @@
|
|||||||
// Delusions of generality, but it's really just for Klammertext commands.
|
// Delusions of generality, but it's really just for Klammertext commands.
|
||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <set>
|
||||||
#include <ranges>
|
#include <ranges>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <regex>
|
#include <regex>
|
||||||
@@ -52,12 +53,23 @@ public:
|
|||||||
void flag(const std::string& name, const std::string& desc);
|
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 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 opt(const std::string& name, const std::string& desc="", const std::string& parameter="", const std::string& default_value="", const std::string& regex_pattern="text");
|
||||||
|
// A variadic option: --name collects every following word up to the
|
||||||
|
// next -/-- token (zero or more). get(name) returns the words
|
||||||
|
// space-joined; given(name) distinguishes "--name with no words"
|
||||||
|
// from an absent --name. Used for subcommand-style interfaces
|
||||||
|
// (kdesc --font install <dir>).
|
||||||
|
void var(const std::string& name, const std::string& desc);
|
||||||
|
|
||||||
void update_width(Arg arg);
|
void update_width(Arg arg);
|
||||||
|
|
||||||
void check_flags_and_options(std::string command, std::vector<std::string>& words);
|
void check_flags_and_options(std::string command, std::vector<std::string>& words);
|
||||||
void parse_flags(std::vector<std::string>& words, std::map<std::string, std::string>& named_args);
|
void parse_flags(std::vector<std::string>& words, std::map<std::string, std::string>& named_args);
|
||||||
void parse_optional(std::vector<std::string>& words, std::map<std::string, std::string>& named_args);
|
void parse_optional(std::vector<std::string>& words, std::map<std::string, std::string>& named_args);
|
||||||
|
void parse_vars(std::vector<std::string>& words, std::map<std::string, std::string>& named_args);
|
||||||
|
|
||||||
|
// True when the named variadic option appeared on the command line
|
||||||
|
// (even with no following words).
|
||||||
|
bool given(const std::string& name) { return m_given.contains(name); };
|
||||||
|
|
||||||
void parse_positional(
|
void parse_positional(
|
||||||
std::string command, // std::vector<std::string> words,
|
std::string command, // std::vector<std::string> words,
|
||||||
@@ -98,6 +110,8 @@ public:
|
|||||||
std::vector<std::string> m_req_names {};
|
std::vector<std::string> m_req_names {};
|
||||||
std::vector<std::string> m_flag_names {};
|
std::vector<std::string> m_flag_names {};
|
||||||
std::vector<std::string> m_opt_names {};
|
std::vector<std::string> m_opt_names {};
|
||||||
|
std::vector<std::string> m_var_names {};
|
||||||
|
std::set<std::string> m_given {};
|
||||||
std::vector<std::string> m_hyphen_markers {};
|
std::vector<std::string> m_hyphen_markers {};
|
||||||
long unsigned int m_syntax_size = 0;
|
long unsigned int m_syntax_size = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
46
mac/env/makefile.env.hollis.DISABLED
vendored
46
mac/env/makefile.env.hollis.DISABLED
vendored
@@ -1,46 +0,0 @@
|
|||||||
KLAMMERTEXT_HOME = /home/ack/projects/klammertext/K
|
|
||||||
|
|
||||||
include $(KLAMMERTEXT_HOME)/mac/env/optimize.env
|
|
||||||
|
|
||||||
CPP_VERSION = c++20
|
|
||||||
|
|
||||||
#GCC_LIB = /usr/lib/gcc/x86_64-linux-gnu/7.4.0
|
|
||||||
CXX = /usr/bin/g++
|
|
||||||
|
|
||||||
#IMPORT = -fmodules -fsearch-include-path bits/std.cc
|
|
||||||
IMPORT =
|
|
||||||
|
|
||||||
# Hack for now; to be generalized:
|
|
||||||
ifneq ("$(wildcard /usr/include/python3.14)","")
|
|
||||||
PYTHON = python3.14
|
|
||||||
else ifneq ("$(wildcard /usr/include/python3.13)","")
|
|
||||||
PYTHON = python3.13
|
|
||||||
else
|
|
||||||
PYTHON = python3.12
|
|
||||||
endif
|
|
||||||
|
|
||||||
# -no-pie?
|
|
||||||
CXXFLAGS = -Wall -Wextra -Weffc++ -fPIC $(PROFILE) -std=$(CPP_VERSION) $(IMPORT) $(OPTIMIZE) \
|
|
||||||
-I$(KLAMMERTEXT_HOME)/mac \
|
|
||||||
-I/usr/include \
|
|
||||||
-I/usr/include/$(PYTHON)
|
|
||||||
|
|
||||||
# -L$(GCC_LIB) \
|
|
||||||
|
|
||||||
LDFLAGS = \
|
|
||||||
-L/usr/lib/x86_64-linux-gnu
|
|
||||||
|
|
||||||
LDLIBS = \
|
|
||||||
-ldl
|
|
||||||
|
|
||||||
ifndef NOPYTHON
|
|
||||||
LDLIBS += -l$(PYTHON)
|
|
||||||
endif
|
|
||||||
|
|
||||||
#GCC_ROOT = /h/dev/pkg/gcc-$(GCC_VERSION)
|
|
||||||
#GCC_LIB = $(GCC_ROOT)/$(GCC_DIR)/lib/gcc/$(GCC_DIR)/$(GCC_VERSION)
|
|
||||||
#$(GCC_LIB)
|
|
||||||
|
|
||||||
LD_LIBRARY_PATH=\
|
|
||||||
/usr/lib64\
|
|
||||||
:/usr/lib/x86_64-linux-gnu
|
|
||||||
33
mac/env/makefile.env.jatke.DISABLED
vendored
33
mac/env/makefile.env.jatke.DISABLED
vendored
@@ -1,33 +0,0 @@
|
|||||||
KLAMMERTEXT_HOME = /home/ack/projects/klammertext/K
|
|
||||||
|
|
||||||
include $(KLAMMERTEXT_HOME)/mac/env/optimize.env
|
|
||||||
|
|
||||||
CPP_VERSION = c++20
|
|
||||||
|
|
||||||
CXX = /usr/bin/g++
|
|
||||||
|
|
||||||
# Hack for now; to be generalized:
|
|
||||||
ifneq ("$(wildcard /usr/include/python3.14)","")
|
|
||||||
PYTHON = python3.14
|
|
||||||
else ifneq ("$(wildcard /usr/include/python3.13)","")
|
|
||||||
PYTHON = python3.13
|
|
||||||
else
|
|
||||||
PYTHON = python3.12
|
|
||||||
endif
|
|
||||||
|
|
||||||
CPPFLAGS = \
|
|
||||||
-I$(KLAMMERTEXT_HOME)/mac \
|
|
||||||
-I/usr/include \
|
|
||||||
-I/usr/include/$(PYTHON)
|
|
||||||
|
|
||||||
CXXFLAGS = -Wall -Wextra -Weffc++ -Wshadow -std=$(CPP_VERSION) -fPIC $(OPTIMIZE) $(SANITIZE)
|
|
||||||
|
|
||||||
LDFLAGS = \
|
|
||||||
-L/usr/lib/x86_64-linux-gnu
|
|
||||||
|
|
||||||
LDLIBS = \
|
|
||||||
-ldl
|
|
||||||
|
|
||||||
ifndef NOPYTHON
|
|
||||||
LDLIBS += -l$(PYTHON)
|
|
||||||
endif
|
|
||||||
33
mac/env/makefile.env.pop.DISABLED
vendored
33
mac/env/makefile.env.pop.DISABLED
vendored
@@ -1,33 +0,0 @@
|
|||||||
KLAMMERTEXT_HOME = /home/ack/projects/klammertext/K
|
|
||||||
|
|
||||||
include $(KLAMMERTEXT_HOME)/mac/env/optimize.env
|
|
||||||
|
|
||||||
CPP_VERSION = c++20
|
|
||||||
|
|
||||||
CXX = /usr/bin/g++
|
|
||||||
|
|
||||||
# Hack for now; to be generalized:
|
|
||||||
ifneq ("$(wildcard /usr/include/python3.14)","")
|
|
||||||
PYTHON = python3.14
|
|
||||||
else ifneq ("$(wildcard /usr/include/python3.13)","")
|
|
||||||
PYTHON = python3.13
|
|
||||||
else
|
|
||||||
PYTHON = python3.12
|
|
||||||
endif
|
|
||||||
|
|
||||||
CPPFLAGS = \
|
|
||||||
-I$(KLAMMERTEXT_HOME)/mac \
|
|
||||||
-I/usr/include \
|
|
||||||
-I/usr/include/$(PYTHON)
|
|
||||||
|
|
||||||
CXXFLAGS = -Wall -Wextra -Weffc++ -Wshadow -std=$(CPP_VERSION) -fPIC $(OPTIMIZE) $(SANITIZE)
|
|
||||||
|
|
||||||
LDFLAGS = \
|
|
||||||
-L/usr/lib/x86_64-linux-gnu
|
|
||||||
|
|
||||||
LDLIBS = \
|
|
||||||
-ldl
|
|
||||||
|
|
||||||
ifndef NOPYTHON
|
|
||||||
LDLIBS += -l$(PYTHON)
|
|
||||||
endif
|
|
||||||
33
mac/env/makefile.env.ubuntu.DISABLED
vendored
33
mac/env/makefile.env.ubuntu.DISABLED
vendored
@@ -1,33 +0,0 @@
|
|||||||
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
|
|
||||||
602
mac/font_store.cpp
Normal file
602
mac/font_store.cpp
Normal file
@@ -0,0 +1,602 @@
|
|||||||
|
// The Klammertext font store: INFRASTRUCTURE, not part of the
|
||||||
|
// Klammermachine (the Machine class never references it) and not part of
|
||||||
|
// any klammer set. Klammer sets (the SKS's @document, a future music
|
||||||
|
// set) consume the store; the kdesc command lists, installs into, and
|
||||||
|
// samples it without loading any klammer set. The store's search runs
|
||||||
|
// over the KLAMMERTEXT_FONTS directories and ends at the distribution's
|
||||||
|
// default font set in $KLAMMERTEXT_HOME/fnt.
|
||||||
|
|
||||||
|
#include "font_store.h"
|
||||||
|
#include "file.h"
|
||||||
|
#include "locator.h"
|
||||||
|
#include "util.h"
|
||||||
|
#include "error.h"
|
||||||
|
#include "log.h"
|
||||||
|
#include "show.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <fstream>
|
||||||
|
#include <regex>
|
||||||
|
#include <set>
|
||||||
|
#include <sstream>
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
|
||||||
|
std::string name_to_dirname(std::string name)
|
||||||
|
{
|
||||||
|
std::string result {};
|
||||||
|
for (char c : name) {
|
||||||
|
if (c == ' ')
|
||||||
|
result += '-';
|
||||||
|
else
|
||||||
|
result += std::tolower(c);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The directories searched for installed fonts: the colon-separated
|
||||||
|
// KLAMMERTEXT_FONTS environment variable, or ~/.klammertext/fonts when it
|
||||||
|
// is not set. Directories are searched in listed order, and the default
|
||||||
|
// fonts in $KLAMMERTEXT_HOME/fnt are searched last, so an installed font
|
||||||
|
// can deliberately shadow a default one.
|
||||||
|
strings_t installed_font_dirs()
|
||||||
|
{
|
||||||
|
strings_t result {};
|
||||||
|
std::string paths {};
|
||||||
|
const char* env = std::getenv("KLAMMERTEXT_FONTS");
|
||||||
|
if (env && *env) {
|
||||||
|
paths = env;
|
||||||
|
} else if (const char* home = std::getenv("HOME"); home && *home) {
|
||||||
|
paths = std::string(home) + "/.klammertext/fonts";
|
||||||
|
}
|
||||||
|
std::stringstream ss(paths);
|
||||||
|
std::string dir;
|
||||||
|
while (std::getline(ss, dir, ':')) {
|
||||||
|
if (!dir.empty()) {
|
||||||
|
result.push_back(dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string default_font_dir()
|
||||||
|
{
|
||||||
|
const char* home = std::getenv(klammertext_home_var.c_str());
|
||||||
|
if (home == nullptr || *home == '\0') {
|
||||||
|
throw Argument_error(
|
||||||
|
"The environment variable " + klammertext_home_var + " is not defined");
|
||||||
|
}
|
||||||
|
return std::string(home) + "/fnt";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void classify_from_css(Resolved_font& font, std::string css_path)
|
||||||
|
{
|
||||||
|
// Parse the @font-face blocks in the CSS to determine variant → filename mapping
|
||||||
|
std::string css = string_from_file(css_path);
|
||||||
|
std::regex face_rgx(
|
||||||
|
R"(@font-face\s*\{[^}]*font-style:\s*(\w+);[^}]*font-weight:\s*(\w+);[^}]*url\('([^']+\.ttf)'\)[^}]*\})",
|
||||||
|
std::regex::multiline);
|
||||||
|
auto begin = std::sregex_iterator(css.begin(), css.end(), face_rgx);
|
||||||
|
auto end = std::sregex_iterator();
|
||||||
|
for (auto it = begin; it != end; ++it) {
|
||||||
|
std::string style = (*it)[1];
|
||||||
|
std::string weight = (*it)[2];
|
||||||
|
std::string url_path = (*it)[3];
|
||||||
|
// URL is like 'dir-name/Filename.ttf' — extract just the filename
|
||||||
|
std::string filename = url_path.substr(url_path.rfind('/') + 1);
|
||||||
|
bool is_bold = (weight == "700" || weight == "bold");
|
||||||
|
bool is_italic = (style == "italic" || style == "oblique");
|
||||||
|
if (is_bold && is_italic)
|
||||||
|
font.bold_italic = filename;
|
||||||
|
else if (is_bold)
|
||||||
|
font.bold = filename;
|
||||||
|
else if (is_italic)
|
||||||
|
font.italic = filename;
|
||||||
|
else
|
||||||
|
font.regular = filename;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve a font within one base directory: <base>/<dir-name>/ holding the
|
||||||
|
// .ttf files and <base>/<dir-name>.css declaring the @font-face variants.
|
||||||
|
static Resolved_font resolve_in_directory(
|
||||||
|
const std::string& family_name, const std::string& dir_name, const std::string& base_dir)
|
||||||
|
{
|
||||||
|
std::string font_dir = base_dir + "/" + dir_name;
|
||||||
|
std::string css_file = font_dir + ".css";
|
||||||
|
if (fs::exists(font_dir) && fs::exists(css_file)) {
|
||||||
|
Resolved_font font {};
|
||||||
|
font.family_name = family_name;
|
||||||
|
font.dir_name = dir_name;
|
||||||
|
font.font_dir = font_dir;
|
||||||
|
font.css_file = css_file;
|
||||||
|
classify_from_css(font, css_file);
|
||||||
|
return font;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every font available for the three @document role parameters: each
|
||||||
|
// <name>.css with a matching <name>/ directory, across the installed
|
||||||
|
// directories and the default font set. The reported name is the family name
|
||||||
|
// from the CSS (what the writer types), falling back to the directory name.
|
||||||
|
strings_t available_font_families()
|
||||||
|
{
|
||||||
|
strings_t result {};
|
||||||
|
static const std::regex family_rgx(R"(font-family:\s*'([^']+)')");
|
||||||
|
auto scan = [&](const std::string& base) {
|
||||||
|
if (!fs::exists(base)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (auto& entry : fs::directory_iterator(base)) {
|
||||||
|
if (entry.path().extension() == ".css" &&
|
||||||
|
fs::is_directory(base + "/" + entry.path().stem().string())) {
|
||||||
|
std::string css = string_from_file(entry.path());
|
||||||
|
std::smatch match {};
|
||||||
|
if (std::regex_search(css, match, family_rgx)) {
|
||||||
|
result.push_back(match[1]);
|
||||||
|
} else {
|
||||||
|
result.push_back(entry.path().stem());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for (const std::string& dir : installed_font_dirs()) {
|
||||||
|
scan(dir);
|
||||||
|
}
|
||||||
|
scan(default_font_dir());
|
||||||
|
std::sort(result.begin(), result.end());
|
||||||
|
result.erase(std::unique(result.begin(), result.end()), result.end());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void extract_font_metrics(Resolved_font& font)
|
||||||
|
{
|
||||||
|
if (font.regular.empty() || font.font_dir.empty())
|
||||||
|
return;
|
||||||
|
std::string ttf_path = font.font_dir + "/" + font.regular;
|
||||||
|
if (!fs::exists(ttf_path))
|
||||||
|
return;
|
||||||
|
// Extract both x-height and cap-height ratios from OS/2 table
|
||||||
|
std::string script =
|
||||||
|
"python3 -c \""
|
||||||
|
"import struct; "
|
||||||
|
"f = open('" + ttf_path + "', 'rb'); "
|
||||||
|
"_, n = struct.unpack('>IH', f.read(6)); "
|
||||||
|
"f.read(6); "
|
||||||
|
"t = {};\n"
|
||||||
|
"for _ in range(n):\n"
|
||||||
|
" tag = f.read(4).decode('latin-1').strip('\\\\x00'); "
|
||||||
|
" _, o, l = struct.unpack('>III', f.read(12)); "
|
||||||
|
" t[tag] = o\n"
|
||||||
|
"f.seek(t['head'] + 18); "
|
||||||
|
"upm = struct.unpack('>H', f.read(2))[0]; "
|
||||||
|
"f.seek(t['OS/2']); "
|
||||||
|
"ver = struct.unpack('>H', f.read(2))[0]; "
|
||||||
|
"f.seek(t['OS/2'] + 86); "
|
||||||
|
"xh, ch = struct.unpack('>hh', f.read(4)); "
|
||||||
|
"print(f'{xh/upm:.4f} {ch/upm:.4f}') if ver >= 2 else None; "
|
||||||
|
"f.close()\"";
|
||||||
|
std::string result = trim(exec(script.c_str()));
|
||||||
|
if (!result.empty()) {
|
||||||
|
try {
|
||||||
|
auto pos = result.find(' ');
|
||||||
|
if (pos != std::string::npos) {
|
||||||
|
font.xheight_ratio = std::stof(result.substr(0, pos));
|
||||||
|
font.capheight_ratio = std::stof(result.substr(pos + 1));
|
||||||
|
}
|
||||||
|
} catch (...) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Resolved_font resolve_font(std::string family_name)
|
||||||
|
{
|
||||||
|
if (family_name.empty())
|
||||||
|
return {};
|
||||||
|
|
||||||
|
std::string dir_name = name_to_dirname(family_name);
|
||||||
|
|
||||||
|
// Installed directories in listed order, then the default font set, so an
|
||||||
|
// installed font can shadow a default one.
|
||||||
|
strings_t bases = installed_font_dirs();
|
||||||
|
bases.push_back(default_font_dir());
|
||||||
|
for (const std::string& base : bases) {
|
||||||
|
Resolved_font font = resolve_in_directory(family_name, dir_name, base);
|
||||||
|
if (!font.family_name.empty()) {
|
||||||
|
extract_font_metrics(font);
|
||||||
|
return font;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::stringstream ss {};
|
||||||
|
ss << "The font \"" << family_name << "\" is not installed.\n\n"
|
||||||
|
<< "Available fonts:\n";
|
||||||
|
for (const std::string& name : available_font_families()) {
|
||||||
|
ss << " " << name << "\n";
|
||||||
|
}
|
||||||
|
ss << "\nFonts are searched in the directories of the KLAMMERTEXT_FONTS\n"
|
||||||
|
<< "environment variable (colon-separated; default $HOME/.klammertext/fonts)\n"
|
||||||
|
<< "and then in the default font set. To install a font,\n"
|
||||||
|
<< "place its files as <fonts-dir>/" << dir_name << "/*.ttf with a\n"
|
||||||
|
<< "<fonts-dir>/" << dir_name << ".css declaring its @font-face variants.";
|
||||||
|
throw Argument_error(ss.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Font file classification: read the family name, weight, and style from
|
||||||
|
// the font's internal tables (sfnt 'name', 'OS/2', 'fvar') rather than
|
||||||
|
// from filenames, which vary by source (Google zips, foundries, ...).
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
uint16_t be16(const std::string& d, size_t off)
|
||||||
|
{
|
||||||
|
return (uint8_t(d[off]) << 8) | uint8_t(d[off + 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t be32(const std::string& d, size_t off)
|
||||||
|
{
|
||||||
|
return (uint32_t(be16(d, off)) << 16) | be16(d, off + 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string read_binary_file(const std::string& path)
|
||||||
|
{
|
||||||
|
std::ifstream in(path, std::ios::binary);
|
||||||
|
std::stringstream ss {};
|
||||||
|
ss << in.rdbuf();
|
||||||
|
return ss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode a name-table string: UTF-16BE for Windows records (keep the BMP
|
||||||
|
// low bytes; family names are almost always Latin), bytes as-is otherwise.
|
||||||
|
std::string decode_name(const std::string& raw, bool utf16be)
|
||||||
|
{
|
||||||
|
std::string result {};
|
||||||
|
if (utf16be) {
|
||||||
|
for (size_t i = 0; i + 1 < raw.size(); i += 2) {
|
||||||
|
if (raw[i] == 0) {
|
||||||
|
result += raw[i + 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result = raw;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Font_file classify_font_file(const std::string& path)
|
||||||
|
{
|
||||||
|
Font_file file {};
|
||||||
|
file.path = path;
|
||||||
|
file.extension = fs::path(path).extension();
|
||||||
|
|
||||||
|
std::string d = read_binary_file(path);
|
||||||
|
if (d.size() < 12) {
|
||||||
|
file.note = "not a font file (too short)";
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
uint32_t tag = be32(d, 0);
|
||||||
|
if (tag == 0x74746366) { // 'ttcf'
|
||||||
|
file.note = "font collections (.ttc) are not supported; "
|
||||||
|
"use the individual font files";
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
if (tag != 0x00010000 && tag != 0x4F54544F) { // sfnt or 'OTTO'
|
||||||
|
file.note = "not a TrueType or OpenType font";
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t num_tables = be16(d, 4);
|
||||||
|
std::map<std::string, std::pair<uint32_t, uint32_t>> tables {}; // tag -> offset,length
|
||||||
|
for (uint16_t i = 0; i < num_tables; i++) {
|
||||||
|
size_t rec = 12 + i * 16;
|
||||||
|
if (rec + 16 > d.size()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
tables[d.substr(rec, 4)] = { be32(d, rec + 8), be32(d, rec + 12) };
|
||||||
|
}
|
||||||
|
file.variable = tables.contains("fvar");
|
||||||
|
|
||||||
|
// Family name from the 'name' table: typographic family (16) wins
|
||||||
|
// over family (1); Windows records (platform 3) win over Macintosh.
|
||||||
|
if (auto it = tables.find("name"); it != tables.end()) {
|
||||||
|
size_t base = it->second.first;
|
||||||
|
uint16_t count = be16(d, base + 2);
|
||||||
|
uint16_t string_offset = be16(d, base + 4);
|
||||||
|
int best_rank = -1;
|
||||||
|
for (uint16_t i = 0; i < count; i++) {
|
||||||
|
size_t rec = base + 6 + i * 12;
|
||||||
|
if (rec + 12 > d.size()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
uint16_t platform = be16(d, rec);
|
||||||
|
uint16_t name_id = be16(d, rec + 6);
|
||||||
|
uint16_t length = be16(d, rec + 8);
|
||||||
|
uint16_t offset = be16(d, rec + 10);
|
||||||
|
if (name_id != 1 && name_id != 16) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int rank = (name_id == 16 ? 2 : 0) + (platform == 3 ? 1 : 0);
|
||||||
|
size_t at = base + string_offset + offset;
|
||||||
|
if (rank > best_rank && at + length <= d.size()) {
|
||||||
|
file.family = decode_name(d.substr(at, length), platform == 3);
|
||||||
|
best_rank = rank;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (file.family.empty()) {
|
||||||
|
file.note = "no family name found in the font's name table";
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool italic = false;
|
||||||
|
if (auto it = tables.find("OS/2"); it != tables.end()) {
|
||||||
|
size_t base = it->second.first;
|
||||||
|
file.weight = be16(d, base + 4);
|
||||||
|
italic = be16(d, base + 62) & 0x0001; // fsSelection italic bit
|
||||||
|
} else if (auto ht = tables.find("head"); ht != tables.end()) {
|
||||||
|
uint16_t mac_style = be16(d, ht->second.first + 44);
|
||||||
|
file.weight = (mac_style & 0x0001) ? 700 : 400;
|
||||||
|
italic = mac_style & 0x0002;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.variable) {
|
||||||
|
// A variable font covers the weight axis; use it as the regular
|
||||||
|
// (or italic) face and let renderers derive weights.
|
||||||
|
file.variant = italic ? "Italic" : "Regular";
|
||||||
|
} else if (file.weight >= 380 && file.weight <= 450) {
|
||||||
|
file.variant = italic ? "Italic" : "Regular";
|
||||||
|
} else if (file.weight >= 650 && file.weight <= 760) {
|
||||||
|
file.variant = italic ? "BoldItalic" : "Bold";
|
||||||
|
} else {
|
||||||
|
std::stringstream note {};
|
||||||
|
note << "weight " << file.weight
|
||||||
|
<< " not installed (only regular 400 and bold 700 are used)";
|
||||||
|
file.note = note.str();
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Font_file> classify_font_files(const std::string& directory)
|
||||||
|
{
|
||||||
|
std::vector<Font_file> result {};
|
||||||
|
if (!fs::exists(directory)) {
|
||||||
|
throw Argument_error(
|
||||||
|
"The font directory \"" + directory + "\" does not exist");
|
||||||
|
}
|
||||||
|
for (auto& entry : fs::recursive_directory_iterator(directory)) {
|
||||||
|
std::string ext = entry.path().extension();
|
||||||
|
if (entry.is_regular_file() && (ext == ".ttf" || ext == ".otf")) {
|
||||||
|
result.push_back(classify_font_file(entry.path()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The css is always generated, never copied, so its urls are relative and
|
||||||
|
// the installed pair stays relocatable.
|
||||||
|
static std::string font_face_css(
|
||||||
|
const std::string& family, const std::string& dir_name,
|
||||||
|
const std::map<std::string, const Font_file*>& slots)
|
||||||
|
{
|
||||||
|
std::stringstream css {};
|
||||||
|
auto emit = [&](const std::string& variant,
|
||||||
|
const std::string& style, const std::string& weight) {
|
||||||
|
auto it = slots.find(variant);
|
||||||
|
if (it == slots.end()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::string format =
|
||||||
|
it->second->extension == ".otf" ? "opentype" : "truetype";
|
||||||
|
css << "\n@font-face {\n"
|
||||||
|
<< " font-family: '" << family << "';\n"
|
||||||
|
<< " font-style: " << style << ";\n"
|
||||||
|
<< " font-weight: " << weight << ";\n"
|
||||||
|
<< " src: url('" << dir_name << "/" << variant
|
||||||
|
<< it->second->extension << "') format('" << format << "');\n"
|
||||||
|
<< "}\n";
|
||||||
|
};
|
||||||
|
emit("Regular", "normal", "400");
|
||||||
|
emit("Bold", "normal", "700");
|
||||||
|
emit("Italic", "italic", "400");
|
||||||
|
emit("BoldItalic", "italic", "700");
|
||||||
|
return css.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string install_fonts(const std::string& source_dir, std::string dest_dir)
|
||||||
|
{
|
||||||
|
if (dest_dir.empty()) {
|
||||||
|
strings_t dirs = installed_font_dirs();
|
||||||
|
if (dirs.empty()) {
|
||||||
|
throw Argument_error(
|
||||||
|
"No installation directory: KLAMMERTEXT_FONTS is empty and "
|
||||||
|
"HOME is not set");
|
||||||
|
}
|
||||||
|
dest_dir = dirs[0];
|
||||||
|
}
|
||||||
|
std::vector<Font_file> files = classify_font_files(source_dir);
|
||||||
|
if (files.empty()) {
|
||||||
|
throw Argument_error(
|
||||||
|
"No font files (.ttf or .otf) found under \"" + source_dir + "\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Choose one file per (family, variant) slot; a static face wins over
|
||||||
|
// a variable font's derived face.
|
||||||
|
std::map<std::string, std::map<std::string, const Font_file*>> families {};
|
||||||
|
std::stringstream report {};
|
||||||
|
for (const Font_file& file : files) {
|
||||||
|
if (file.variant.empty()) {
|
||||||
|
report << " skipped " << fs::path(file.path).filename().string()
|
||||||
|
<< ": " << file.note << "\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto& slots = families[file.family];
|
||||||
|
auto it = slots.find(file.variant);
|
||||||
|
if (it == slots.end() || (it->second->variable && !file.variable)) {
|
||||||
|
slots[file.variant] = &file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& [family, slots] : families) {
|
||||||
|
std::string dir_name = name_to_dirname(family);
|
||||||
|
std::string family_dir = dest_dir + "/" + dir_name;
|
||||||
|
fs::create_directories(family_dir);
|
||||||
|
strings_t variants {};
|
||||||
|
for (auto& [variant, file] : slots) {
|
||||||
|
copy_file_stream(file->path,
|
||||||
|
family_dir + "/" + variant + file->extension);
|
||||||
|
variants.push_back(variant + (file->variable ? " (variable)" : ""));
|
||||||
|
}
|
||||||
|
string_to_file(dest_dir + "/" + dir_name + ".css",
|
||||||
|
font_face_css(family, dir_name, slots));
|
||||||
|
report << " installed \"" << family << "\" (" << join(variants, ", ")
|
||||||
|
<< ") in " << family_dir << "\n";
|
||||||
|
}
|
||||||
|
return report.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string describe_fonts()
|
||||||
|
{
|
||||||
|
std::stringstream ss {};
|
||||||
|
std::set<std::string> seen {};
|
||||||
|
strings_t bases = installed_font_dirs();
|
||||||
|
bases.push_back(default_font_dir());
|
||||||
|
for (size_t i = 0; i < bases.size(); i++) {
|
||||||
|
const std::string& base = bases[i];
|
||||||
|
bool is_default = (i == bases.size() - 1);
|
||||||
|
ss << base << (is_default ? " (default font set)" : "") << ":\n";
|
||||||
|
if (!fs::exists(base)) {
|
||||||
|
ss << " [directory does not exist]\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
strings_t names {};
|
||||||
|
for (auto& entry : fs::directory_iterator(base)) {
|
||||||
|
if (entry.path().extension() == ".css" &&
|
||||||
|
fs::is_directory(base + "/" + entry.path().stem().string())) {
|
||||||
|
names.push_back(entry.path().stem());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::sort(names.begin(), names.end());
|
||||||
|
if (names.empty()) {
|
||||||
|
ss << " [no fonts]\n";
|
||||||
|
}
|
||||||
|
for (const std::string& dir_name : names) {
|
||||||
|
Resolved_font font = resolve_in_directory("?", dir_name, base);
|
||||||
|
std::string css = string_from_file(font.css_file);
|
||||||
|
std::smatch match {};
|
||||||
|
std::string family = dir_name;
|
||||||
|
if (std::regex_search(css, match,
|
||||||
|
std::regex(R"(font-family:\s*'([^']+)')"))) {
|
||||||
|
family = match[1];
|
||||||
|
}
|
||||||
|
strings_t variants {};
|
||||||
|
if (!font.regular.empty()) variants.push_back("Regular");
|
||||||
|
if (!font.bold.empty()) variants.push_back("Bold");
|
||||||
|
if (!font.italic.empty()) variants.push_back("Italic");
|
||||||
|
if (!font.bold_italic.empty()) variants.push_back("BoldItalic");
|
||||||
|
ss << " " << family << " (" << join(variants, ", ") << ")";
|
||||||
|
if (seen.contains(dir_name)) {
|
||||||
|
ss << " [shadowed by an earlier directory]";
|
||||||
|
}
|
||||||
|
seen.insert(dir_name);
|
||||||
|
ss << "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string write_font_samples(const std::string& output_dir, const std::string& source_dir)
|
||||||
|
{
|
||||||
|
fs::create_directories(output_dir);
|
||||||
|
strings_t families {};
|
||||||
|
if (source_dir.empty()) {
|
||||||
|
for (const std::string& family : available_font_families()) {
|
||||||
|
install_resolved_font(resolve_font(family), output_dir);
|
||||||
|
families.push_back(family);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Uninstalled preview: install the classified fonts directly into
|
||||||
|
// the sample page's own fonts directory.
|
||||||
|
install_fonts(source_dir, output_dir + "/fonts");
|
||||||
|
for (auto& entry : fs::directory_iterator(output_dir + "/fonts")) {
|
||||||
|
if (entry.path().extension() == ".css") {
|
||||||
|
std::string css = string_from_file(entry.path());
|
||||||
|
std::smatch match {};
|
||||||
|
if (std::regex_search(css, match,
|
||||||
|
std::regex(R"(font-family:\s*'([^']+)')"))) {
|
||||||
|
families.push_back(match[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::sort(families.begin(), families.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::stringstream html {};
|
||||||
|
html << "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n"
|
||||||
|
<< "<meta charset=\"UTF-8\">\n<title>Klammertext font samples</title>\n";
|
||||||
|
for (const std::string& family : families) {
|
||||||
|
html << "<link href=\"fonts/" << name_to_dirname(family)
|
||||||
|
<< ".css\" rel=\"stylesheet\">\n";
|
||||||
|
}
|
||||||
|
html << "<style>\n"
|
||||||
|
<< "body { margin: 2rem auto; max-width: 46rem; font-family: sans-serif; }\n"
|
||||||
|
<< "h2 { border-bottom: 1px solid #999; margin-top: 2.5rem; }\n"
|
||||||
|
<< ".sample { font-size: 1.3rem; margin: .3rem 0; }\n"
|
||||||
|
<< ".alphabet { font-size: 1.0rem; color: #333; margin: .3rem 0; }\n"
|
||||||
|
<< "</style>\n</head>\n<body>\n<h1>Klammertext font samples</h1>\n";
|
||||||
|
for (const std::string& family : families) {
|
||||||
|
html << "<h2>" << family << "</h2>\n"
|
||||||
|
<< "<div style=\"font-family: '" << family << "'\">\n"
|
||||||
|
<< "<p class=\"sample\">The quick brown fox jumps over the lazy dog.</p>\n"
|
||||||
|
<< "<p class=\"sample\" style=\"font-style: italic\">"
|
||||||
|
<< "The quick brown fox jumps over the lazy dog.</p>\n"
|
||||||
|
<< "<p class=\"sample\" style=\"font-weight: bold\">"
|
||||||
|
<< "The quick brown fox jumps over the lazy dog.</p>\n"
|
||||||
|
<< "<p class=\"sample\" style=\"font-weight: bold; font-style: italic\">"
|
||||||
|
<< "The quick brown fox jumps over the lazy dog.</p>\n"
|
||||||
|
<< "<p class=\"alphabet\">ABCDEFGHIJKLMNOPQRSTUVWXYZ "
|
||||||
|
<< "abcdefghijklmnopqrstuvwxyz 0123456789 "
|
||||||
|
<< "äöüß “quoted” 3.14159</p>\n"
|
||||||
|
<< "</div>\n";
|
||||||
|
}
|
||||||
|
html << "</body>\n</html>\n";
|
||||||
|
std::string index = output_dir + "/index.html";
|
||||||
|
string_to_file(index, html.str());
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Font assets are copied with copy_file_stream() (mac/file.h) rather than
|
||||||
|
// std::filesystem::copy_file, which fails on Apple `container` virtiofs mounts
|
||||||
|
// — see the note on copy_file_stream() in file.cpp for the full rationale.
|
||||||
|
|
||||||
|
|
||||||
|
void install_resolved_font(const Resolved_font& font, std::string output_dir)
|
||||||
|
{
|
||||||
|
if (font.family_name.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
std::string output_font_dir = output_dir + "/fonts";
|
||||||
|
if (!fs::exists(output_font_dir))
|
||||||
|
fs::create_directory(output_font_dir);
|
||||||
|
|
||||||
|
// Copy .css file and font directory to output
|
||||||
|
std::string dest_css = output_font_dir + "/" + font.dir_name + ".css";
|
||||||
|
std::string dest_dir = output_font_dir + "/" + font.dir_name;
|
||||||
|
|
||||||
|
copy_file_stream(font.css_file, dest_css);
|
||||||
|
|
||||||
|
if (!fs::exists(dest_dir)) {
|
||||||
|
fs::create_directory(dest_dir);
|
||||||
|
for (auto& entry : fs::directory_iterator(font.font_dir)) {
|
||||||
|
copy_file_stream(entry.path(),
|
||||||
|
dest_dir + "/" + entry.path().filename().string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
59
mac/font_store.h
Normal file
59
mac/font_store.h
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// The Klammertext font store (infrastructure; see font_store.cpp).
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
struct Resolved_font {
|
||||||
|
std::string family_name {}; // "Crimson Pro"
|
||||||
|
std::string dir_name {}; // "crimson-pro"
|
||||||
|
std::string font_dir {}; // Full path to font directory
|
||||||
|
std::string css_file {}; // Full path to .css file
|
||||||
|
// .ttf filenames for each variant (empty if variant not available):
|
||||||
|
std::string regular {};
|
||||||
|
std::string bold {};
|
||||||
|
std::string italic {};
|
||||||
|
std::string bold_italic {};
|
||||||
|
float xheight_ratio = 0.0f; // x-height / unitsPerEm from OS/2 table
|
||||||
|
float capheight_ratio = 0.0f; // cap-height / unitsPerEm from OS/2 table
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string name_to_dirname(std::string name);
|
||||||
|
// Directories searched for installed fonts (KLAMMERTEXT_FONTS, default
|
||||||
|
// ~/.klammertext/fonts); the default font set is searched after them.
|
||||||
|
std::vector<std::string> installed_font_dirs();
|
||||||
|
// The distribution's default fonts: $KLAMMERTEXT_HOME/fnt.
|
||||||
|
std::string default_font_dir();
|
||||||
|
// Every installable font family found across those directories.
|
||||||
|
std::vector<std::string> available_font_families();
|
||||||
|
Resolved_font resolve_font(std::string family_name);
|
||||||
|
void install_resolved_font(const Resolved_font& font, std::string output_dir);
|
||||||
|
|
||||||
|
// One font file classified by its internal metadata (name table, OS/2).
|
||||||
|
struct Font_file {
|
||||||
|
std::string path {};
|
||||||
|
std::string family {}; // from name table (nameID 16, else 1)
|
||||||
|
std::string variant {}; // Regular | Bold | Italic | BoldItalic
|
||||||
|
std::string extension {}; // ".ttf" or ".otf"
|
||||||
|
bool variable = false; // has an 'fvar' table
|
||||||
|
int weight = 0; // OS/2 usWeightClass
|
||||||
|
std::string note {}; // reason when the file is not installable
|
||||||
|
};
|
||||||
|
|
||||||
|
// Recursively classify every .ttf/.otf under a directory.
|
||||||
|
std::vector<Font_file> classify_font_files(const std::string& directory);
|
||||||
|
|
||||||
|
// Install the classified families from source_dir into dest_dir (default:
|
||||||
|
// the first KLAMMERTEXT_FONTS directory, created if necessary), in the
|
||||||
|
// canonical relocatable layout <dest>/<family-kebab>/{Variant}.ttf plus a
|
||||||
|
// generated <family-kebab>.css with relative urls. Returns a report.
|
||||||
|
std::string install_fonts(const std::string& source_dir, std::string dest_dir = "");
|
||||||
|
|
||||||
|
// Describe every font in the store with provenance and variants.
|
||||||
|
std::string describe_fonts();
|
||||||
|
|
||||||
|
// Write an HTML specimen page for fonts into output_dir. With an empty
|
||||||
|
// source_dir, samples every available font in the store; otherwise
|
||||||
|
// classifies and samples the (possibly uninstalled) fonts in source_dir.
|
||||||
|
std::string write_font_samples(const std::string& output_dir, const std::string& source_dir = "");
|
||||||
@@ -405,7 +405,7 @@ katom_list Machine::apply_klammer(
|
|||||||
katom_list result(klammer.m_body[target].begin(), klammer.m_body[target].end());
|
katom_list result(klammer.m_body[target].begin(), klammer.m_body[target].end());
|
||||||
auto varmap = klammer.m_varmap[target];
|
auto varmap = klammer.m_varmap[target];
|
||||||
m_state.open_frame("Arguments for klammer " + q_(klammer.m_name));
|
m_state.open_frame("Arguments for klammer " + q_(klammer.m_name));
|
||||||
m_state.set(values);
|
m_state.set(values, klammer.m_parameters);
|
||||||
for (const auto& [name, indices] : varmap) {
|
for (const auto& [name, indices] : varmap) {
|
||||||
std::regex arg("\\*" + name + "\\*");
|
std::regex arg("\\*" + name + "\\*");
|
||||||
for (auto i : indices) {
|
for (auto i : indices) {
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ std::vector<std::string> Frame::names() const
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Frame::set(std::string name, std::string value,
|
void Frame::set(std::string name, std::string value,
|
||||||
std::string delim, std::string desc, Locator loc)
|
std::string delim, std::string desc, Locator loc, Argtype argtype)
|
||||||
{
|
{
|
||||||
Var v(name, value, delim, desc, loc);
|
Var v(name, value, delim, desc, loc, argtype);
|
||||||
m_vars[name] = v;
|
m_vars[name] = v;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ void State::close_frame()
|
|||||||
}
|
}
|
||||||
|
|
||||||
void State::set(std::string name, std::string value, bool update,
|
void State::set(std::string name, std::string value, bool update,
|
||||||
std::string delim, std::string desc, Locator loc)
|
std::string delim, std::string desc, Locator loc, Argtype argtype)
|
||||||
{
|
{
|
||||||
if (m_frames.empty()) {
|
if (m_frames.empty()) {
|
||||||
std::stringstream ss {};
|
std::stringstream ss {};
|
||||||
@@ -86,7 +86,7 @@ void State::set(std::string name, std::string value, bool update,
|
|||||||
<< q_(current.m_value) << ".";
|
<< q_(current.m_value) << ".";
|
||||||
throw Argument_error(ss.str(), current.m_loc);
|
throw Argument_error(ss.str(), current.m_loc);
|
||||||
}
|
}
|
||||||
m_frames[0].set(name, value, delim, desc, loc);
|
m_frames[0].set(name, value, delim, desc, loc, argtype);
|
||||||
}
|
}
|
||||||
|
|
||||||
void State::set(std::map<std::string, std::string> varmap)
|
void State::set(std::map<std::string, std::string> varmap)
|
||||||
@@ -96,6 +96,16 @@ void State::set(std::map<std::string, std::string> varmap)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void State::set(const std::map<std::string, std::string>& varmap,
|
||||||
|
const Parameter_set& parameters)
|
||||||
|
{
|
||||||
|
for (const auto& [name, value] : varmap) {
|
||||||
|
const Parameter* parameter = parameters.find(name);
|
||||||
|
set(name, value, false, " ", "", Locator(),
|
||||||
|
parameter ? parameter->m_argtype : Argtype());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void State::replace(std::string name, std::string value, bool error_if_not_defined)
|
void State::replace(std::string name, std::string value, bool error_if_not_defined)
|
||||||
{
|
{
|
||||||
@@ -269,13 +279,8 @@ std::string State::python_code()
|
|||||||
<< margin << std::left << std::setw(name_length) << "K_eval_id" << " = "
|
<< margin << std::left << std::setw(name_length) << "K_eval_id" << " = "
|
||||||
<< State::class_id++ << "\n";
|
<< State::class_id++ << "\n";
|
||||||
for (const auto& name : names) {
|
for (const auto& name : names) {
|
||||||
// auto [var_value, argtype] = value_type(name);
|
Var var = get(name);
|
||||||
// ss << argtype.python_value(name, var_value, name_length) << "\n";
|
ss << var.m_argtype.python_value(name, {var.m_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";
|
// msg() << ss.str() << "\n";
|
||||||
|
|||||||
13
mac/state.h
13
mac/state.h
@@ -15,12 +15,14 @@ class Var
|
|||||||
public:
|
public:
|
||||||
Var() = default;
|
Var() = default;
|
||||||
Var(std::string name, std::string value=klammerstate::no_value,
|
Var(std::string name, std::string value=klammerstate::no_value,
|
||||||
std::string delim=":", std::string desc="", Locator loc=Locator())
|
std::string delim=":", std::string desc="", Locator loc=Locator(),
|
||||||
|
Argtype argtype=Argtype())
|
||||||
: m_name(name)
|
: m_name(name)
|
||||||
, m_value(value)
|
, m_value(value)
|
||||||
, m_delim(delim)
|
, m_delim(delim)
|
||||||
, m_desc(desc)
|
, m_desc(desc)
|
||||||
, m_loc(loc)
|
, m_loc(loc)
|
||||||
|
, m_argtype(argtype)
|
||||||
{};
|
{};
|
||||||
bool defined();
|
bool defined();
|
||||||
|
|
||||||
@@ -29,6 +31,7 @@ public:
|
|||||||
std::string m_delim {};
|
std::string m_delim {};
|
||||||
std::string m_desc {};
|
std::string m_desc {};
|
||||||
Locator m_loc {};
|
Locator m_loc {};
|
||||||
|
Argtype m_argtype {};
|
||||||
};
|
};
|
||||||
|
|
||||||
namespace klammerstate {
|
namespace klammerstate {
|
||||||
@@ -44,7 +47,8 @@ public:
|
|||||||
|
|
||||||
std::vector<std::string> names() const;
|
std::vector<std::string> names() const;
|
||||||
void set(std::string name, std::string value,
|
void set(std::string name, std::string value,
|
||||||
std::string delim=":", std::string desc="", Locator loc=Locator());
|
std::string delim=":", std::string desc="", Locator loc=Locator(),
|
||||||
|
Argtype argtype=Argtype());
|
||||||
std::pair<Var, bool> get(std::string name);
|
std::pair<Var, bool> get(std::string name);
|
||||||
|
|
||||||
std::string m_name {};
|
std::string m_name {};
|
||||||
@@ -58,8 +62,11 @@ public:
|
|||||||
void open_frame(std::string name);
|
void open_frame(std::string name);
|
||||||
void close_frame();
|
void close_frame();
|
||||||
void set(std::string name, std::string value, bool update=false,
|
void set(std::string name, std::string value, bool update=false,
|
||||||
std::string delim=" ", std::string desc="", Locator loc=Locator());
|
std::string delim=" ", std::string desc="", Locator loc=Locator(),
|
||||||
|
Argtype argtype=Argtype());
|
||||||
void set(std::map<std::string, std::string> varmap);
|
void set(std::map<std::string, std::string> varmap);
|
||||||
|
void set(const std::map<std::string, std::string>& varmap,
|
||||||
|
const Parameter_set& parameters);
|
||||||
void replace(std::string name, std::string value, bool error_if_not_defined=true);
|
void replace(std::string name, std::string value, bool error_if_not_defined=true);
|
||||||
void add_environment_frame();
|
void add_environment_frame();
|
||||||
Var get(std::string name, bool error_if_not_defined=false, Locator loc=Locator());
|
Var get(std::string name, bool error_if_not_defined=false, Locator loc=Locator());
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ K := $(KLAMMERTEXT_HOME)
|
|||||||
KS := $(K)/sks
|
KS := $(K)/sks
|
||||||
KM := $(K)/mac
|
KM := $(K)/mac
|
||||||
|
|
||||||
include $(KM)/env/makefile.env
|
include $(K)/env/makefile.env
|
||||||
|
|
||||||
# Additional include paths for sks components
|
# Additional include paths for sks components
|
||||||
LOCAL_CPPFLAGS := -I$(KM) -I$(KS)/kutil -I$(KS)/target
|
LOCAL_CPPFLAGS := -I$(KM) -I$(KS)/kutil -I$(KS)/target
|
||||||
@@ -23,8 +23,7 @@ LOCAL_DEPFILES := document_class.d document_html.d document_latex.d heading.d re
|
|||||||
|
|
||||||
# External object files from sks/ (mac/*.o now in libklammertext.so)
|
# External object files from sks/ (mac/*.o now in libklammertext.so)
|
||||||
SKS_OBJECTS := $(KS)/kutil/kutil.o $(KS)/kutil/klammer_base.o \
|
SKS_OBJECTS := $(KS)/kutil/kutil.o $(KS)/kutil/klammer_base.o \
|
||||||
$(KS)/target/html_util.o $(KS)/target/latex_util.o \
|
$(KS)/target/html_util.o $(KS)/target/latex_util.o
|
||||||
$(KS)/target/font_resolve.o
|
|
||||||
|
|
||||||
ALL_OBJECTS := $(LOCAL_OBJECTS) $(SKS_OBJECTS)
|
ALL_OBJECTS := $(LOCAL_OBJECTS) $(SKS_OBJECTS)
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
@@@argtype document_structure |
|
@@@argtype document_structure |
|
||||||
Structure of a document: plain, article, or book
|
Structure of a document: plain, article, or book
|
||||||
:pattern plain^|article^|book
|
:pattern plain^|article^|book
|
||||||
|
:default plain
|
||||||
@@@
|
@@@
|
||||||
|
|
||||||
|
|
||||||
@@document.k
|
@@document.k
|
||||||
:title
|
:title
|
||||||
:subtitle
|
:subtitle
|
||||||
@@ -19,7 +21,7 @@
|
|||||||
:toc.bool false
|
:toc.bool false
|
||||||
]#
|
]#
|
||||||
|
|
||||||
:structure.document_structure plain
|
:structure.document_structure
|
||||||
|
|
||||||
:text
|
:text
|
||||||
:files
|
:files
|
||||||
@@ -38,9 +40,6 @@
|
|||||||
:include_sks_js.bool true
|
:include_sks_js.bool true
|
||||||
:include_fonts.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
|
:serif_font Crimson Pro
|
||||||
:sans_font Open Sans
|
:sans_font Open Sans
|
||||||
:mono_font Inconsolata
|
:mono_font Inconsolata
|
||||||
|
|||||||
@@ -62,8 +62,6 @@ Document_class::Document_class(Machine& machine) : Klammer_base(machine)
|
|||||||
m_include_sks_js = strbool(get("include_sks_js"), loc);
|
m_include_sks_js = strbool(get("include_sks_js"), loc);
|
||||||
|
|
||||||
// font_dirs = word_split(get("font_dirs"));
|
// 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) {
|
auto strip_quotes = [](std::string s) {
|
||||||
if (s.size() >= 2 && s.front() == '"' && s.back() == '"')
|
if (s.size() >= 2 && s.front() == '"' && s.back() == '"')
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
#include "latex_util.h"
|
#include "latex_util.h"
|
||||||
#include "machine.h"
|
#include "machine.h"
|
||||||
#include "heading.h"
|
#include "heading.h"
|
||||||
#include "font_resolve.h"
|
#include "font_store.h"
|
||||||
|
|
||||||
const std::string closed_symbol { "&^#9656;" };
|
const std::string closed_symbol { "&^#9656;" };
|
||||||
const std::string open_symbol { "&^#9662;" };
|
const std::string open_symbol { "&^#9662;" };
|
||||||
@@ -120,8 +120,6 @@ public:
|
|||||||
bool m_include_sks_js = true;
|
bool m_include_sks_js = true;
|
||||||
|
|
||||||
strings_t m_font_dirs {};
|
strings_t m_font_dirs {};
|
||||||
strings_t m_local_fonts {};
|
|
||||||
strings_t m_google_fonts {};
|
|
||||||
|
|
||||||
std::string m_serif_font {};
|
std::string m_serif_font {};
|
||||||
std::string m_sans_font {};
|
std::string m_sans_font {};
|
||||||
|
|||||||
@@ -87,8 +87,7 @@ strings_t Document_class::resolved_font_names()
|
|||||||
{
|
{
|
||||||
strings_t result {};
|
strings_t result {};
|
||||||
auto add = [&](const Resolved_font& rf) {
|
auto add = [&](const Resolved_font& rf) {
|
||||||
if (!rf.family_name.empty() &&
|
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);
|
result.push_back(rf.dir_name);
|
||||||
};
|
};
|
||||||
add(m_resolved_serif);
|
add(m_resolved_serif);
|
||||||
@@ -101,13 +100,17 @@ std::string Document_class::font_definitions()
|
|||||||
{
|
{
|
||||||
std::stringstream ss {};
|
std::stringstream ss {};
|
||||||
if (!m_serif_font.empty() || !m_sans_font.empty() || !m_mono_font.empty()) {
|
if (!m_serif_font.empty() || !m_sans_font.empty() || !m_mono_font.empty()) {
|
||||||
|
// Family names are quoted: an unquoted name with a digit-initial
|
||||||
|
// word ("Source Sans 3") is invalid CSS, and a font-family using
|
||||||
|
// var() with such a value computes to inherit, silently losing
|
||||||
|
// the font.
|
||||||
ss << ":root {\n";
|
ss << ":root {\n";
|
||||||
if (!m_serif_font.empty())
|
if (!m_serif_font.empty())
|
||||||
ss << " --serif: " << m_serif_font << ", serif;\n";
|
ss << " --serif: \"" << m_serif_font << "\", serif;\n";
|
||||||
if (!m_sans_font.empty())
|
if (!m_sans_font.empty())
|
||||||
ss << " --sans-serif: " << m_sans_font << ", sans-serif;\n";
|
ss << " --sans-serif: \"" << m_sans_font << "\", sans-serif;\n";
|
||||||
if (!m_mono_font.empty())
|
if (!m_mono_font.empty())
|
||||||
ss << " --monospace: " << m_mono_font << ", monospace;\n";
|
ss << " --monospace: \"" << m_mono_font << "\", monospace;\n";
|
||||||
ss << "}\n";
|
ss << "}\n";
|
||||||
}
|
}
|
||||||
// Emit scale factors so sans and mono fonts match the serif font.
|
// Emit scale factors so sans and mono fonts match the serif font.
|
||||||
@@ -180,7 +183,6 @@ std::string Document_class::create_html_output_directories()
|
|||||||
if (!file_exists(output_directory)) {
|
if (!file_exists(output_directory)) {
|
||||||
fs::create_directory(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_serif, output_directory);
|
||||||
install_resolved_font(m_resolved_sans, output_directory);
|
install_resolved_font(m_resolved_sans, output_directory);
|
||||||
install_resolved_font(m_resolved_mono, output_directory);
|
install_resolved_font(m_resolved_mono, output_directory);
|
||||||
@@ -665,13 +667,9 @@ elements_t Document_class::page(std::string body_text, std::string output_dir, i
|
|||||||
|
|
||||||
std::string page_title = m_page_title.empty() ? m_title : m_page_title;
|
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(
|
elements_t page = html::make_page(
|
||||||
body, page_title,
|
body, page_title,
|
||||||
css_text, css_filenames, all_local_fonts, m_google_fonts);
|
css_text, css_filenames, resolved_font_names());
|
||||||
|
|
||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
@@ -734,10 +732,7 @@ std::string Document_class::make_html_navigation_structure(
|
|||||||
m_file_components[0].second,
|
m_file_components[0].second,
|
||||||
m_date, m_version, m_copyright,
|
m_date, m_version, m_copyright,
|
||||||
css_text, all_css_files, all_js_files,
|
css_text, all_css_files, all_js_files,
|
||||||
[&]() { strings_t f = m_local_fonts;
|
resolved_font_names(),
|
||||||
for (auto& n : resolved_font_names()) f.push_back(n);
|
|
||||||
return f; }(),
|
|
||||||
m_google_fonts,
|
|
||||||
m_logo);
|
m_logo);
|
||||||
std::string result = t.str();
|
std::string result = t.str();
|
||||||
result = string_replace(result,
|
result = string_replace(result,
|
||||||
|
|||||||
@@ -12,9 +12,10 @@
|
|||||||
'b' (bold), 's' (sans-serif), 't' (typewriter or monospace) or 'c'
|
'b' (bold), 's' (sans-serif), 't' (typewriter or monospace) or 'c'
|
||||||
(code) for each cell in a row. If there are fewer font symbols than
|
(code) for each cell in a row. If there are fewer font symbols than
|
||||||
cells in a row, the last symbol is repeated. Extra symbols are
|
cells in a row, the last symbol is repeated. Extra symbols are
|
||||||
ignored. Default is 'r'
|
ignored.
|
||||||
:pattern ('font'^|\s)+
|
:pattern ('font'^|\s)+
|
||||||
:python_cast (lambda s : s.split())
|
:python_cast (lambda s : s.split())
|
||||||
|
:default r
|
||||||
@@@
|
@@@
|
||||||
|
|
||||||
@@font.k fontname : Switch to an available font for PDF output. @@
|
@@font.k fontname : Switch to an available font for PDF output. @@
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ def html_fontify(text, font_symbol, font_size):
|
|||||||
if cls:
|
if cls:
|
||||||
if font_symbol == "c":
|
if font_symbol == "c":
|
||||||
result = re.sub(" ", " ", result)
|
result = re.sub(" ", " ", result)
|
||||||
cls = f'class="cls"'
|
cls = f'class="{cls}"'
|
||||||
sty = ""
|
sty = ""
|
||||||
if font_size != 1:
|
if font_size != 1:
|
||||||
sty = f'style="font-size: {float(font_size) * 100}%"'
|
sty = f'style="font-size: {float(font_size) * 100}%"'
|
||||||
|
|||||||
@@ -16,6 +16,13 @@
|
|||||||
# @@@argtype pixels | a pixel count :pattern \d+px @@@
|
# @@@argtype pixels | a pixel count :pattern \d+px @@@
|
||||||
# @@image.k basename : Image read from a file @@
|
# @@image.k basename : Image read from a file @@
|
||||||
# @@image.html basename | width | height : @eval import image ; result = image.image(K) eval@ @@
|
# @@image.html basename | width | height : @eval import image ; result = image.image(K) eval@ @@
|
||||||
|
|
||||||
|
@@@argtype image_hpos |
|
||||||
|
horizontal position of an image
|
||||||
|
:pattern left^|center^|right^|none
|
||||||
|
:default center
|
||||||
|
@@@
|
||||||
|
|
||||||
@@image
|
@@image
|
||||||
basename
|
basename
|
||||||
:id
|
:id
|
||||||
@@ -23,7 +30,7 @@
|
|||||||
@caption_arguments@
|
@caption_arguments@
|
||||||
:vmargin.bool true
|
:vmargin.bool true
|
||||||
:center.bool true
|
:center.bool true
|
||||||
:hpos.hpos center
|
:hpos.image_hpos
|
||||||
:rel
|
:rel
|
||||||
:abswidth.number 0.0
|
:abswidth.number 0.0
|
||||||
:border.bool false
|
:border.bool false
|
||||||
@@ -32,13 +39,13 @@
|
|||||||
@@
|
@@
|
||||||
|
|
||||||
@@image_grid
|
@@image_grid
|
||||||
image_specs.rest
|
image_specs.rest(2)
|
||||||
:caption
|
:caption
|
||||||
:number.bool true
|
:number.bool true
|
||||||
:cell_number.bool false
|
:cell_number.bool false
|
||||||
:landscape.bool false
|
:landscape.bool false
|
||||||
:scale.number 0.98
|
:scale.number 0.98
|
||||||
:caption_side.side bottom
|
:caption_side.caption_side
|
||||||
:caption_side_center.bool true
|
:caption_side_center.bool true
|
||||||
#
|
#
|
||||||
:thumbnail.bool false
|
:thumbnail.bool false
|
||||||
|
|||||||
@@ -122,7 +122,6 @@ class Image(klammer_base.Klammer_base):
|
|||||||
#if self.caption or self.number:
|
#if self.caption or self.number:
|
||||||
# width = "\\textwidth"
|
# width = "\\textwidth"
|
||||||
#else:
|
#else:
|
||||||
self.number = self.number == "true"
|
|
||||||
width = kutil.parse_length("tex", self.width, self.rel_fraction)[0]
|
width = kutil.parse_length("tex", self.width, self.rel_fraction)[0]
|
||||||
width = re.sub("px", "pt", width)
|
width = re.sub("px", "pt", width)
|
||||||
#result = f'\\includegraphics[width={width}]{{{source}}}'
|
#result = f'\\includegraphics[width={width}]{{{source}}}'
|
||||||
|
|||||||
@@ -25,11 +25,16 @@ class Kargs:
|
|||||||
self.K_input_filenames = K.K_input_filenames
|
self.K_input_filenames = K.K_input_filenames
|
||||||
self.K_output_dir = K.K_output_dir
|
self.K_output_dir = K.K_output_dir
|
||||||
self.K_output_basename = K.K_output_basename
|
self.K_output_basename = K.K_output_basename
|
||||||
self.image_output_dir = K.Image_output_dir
|
self.Image_output_dir = K.Image_output_dir
|
||||||
self.K_toc_only = False
|
self.K_toc_only = False
|
||||||
self.id = ""
|
self.id = ""
|
||||||
self.as_string = False
|
self.as_string = False
|
||||||
self.rel = None
|
self.rel = None
|
||||||
|
# Defaults for @image parameters the grid does not use per cell.
|
||||||
|
self.border = False
|
||||||
|
self.vmargin = False
|
||||||
|
self.abswidth = 0.0
|
||||||
|
self.caption_font_size = .9
|
||||||
|
|
||||||
def img(K, target, basename, caption, width):
|
def img(K, target, basename, caption, width):
|
||||||
result = image.Image(
|
result = image.Image(
|
||||||
@@ -46,7 +51,7 @@ class Image_grid(klammer_base.Klammer_base):
|
|||||||
self.basenames = []
|
self.basenames = []
|
||||||
self.captions = []
|
self.captions = []
|
||||||
|
|
||||||
for row in kutil.rest_args(self.image_specs, dimensions=2):
|
for row in self.image_specs:
|
||||||
self.images.append(
|
self.images.append(
|
||||||
[e.groups() for e in [image_pat.fullmatch(s.strip()) for s in row]])
|
[e.groups() for e in [image_pat.fullmatch(s.strip()) for s in row]])
|
||||||
self.basenames.append([e[0] for e in self.images[-1]])
|
self.basenames.append([e[0] for e in self.images[-1]])
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
K := $(KLAMMERTEXT_HOME)
|
K := $(KLAMMERTEXT_HOME)
|
||||||
KM := $(K)/mac
|
KM := $(K)/mac
|
||||||
|
|
||||||
include $(KM)/env/makefile.env
|
include $(K)/env/makefile.env
|
||||||
|
|
||||||
# Source files
|
# Source files
|
||||||
SOURCES := kutil.cpp klammer_base.cpp
|
SOURCES := kutil.cpp klammer_base.cpp
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
@@show s : @eval :cpp show show @ @@
|
@@show s : @eval :cpp show show @ @@
|
||||||
|
|
||||||
|
@@@argtype caption_side |
|
||||||
|
the side of its element on which a caption is placed
|
||||||
|
:pattern top^|right^|bottom^|left
|
||||||
|
:default bottom
|
||||||
|
@@@
|
||||||
|
|
||||||
@@caption_arguments :
|
@@caption_arguments :
|
||||||
:caption
|
:caption
|
||||||
:number.bool true
|
:number.bool true
|
||||||
:caption_side.side bottom
|
:caption_side.caption_side
|
||||||
:caption_font.font i
|
:caption_font.font i
|
||||||
:caption_font_size.float .9
|
:caption_font_size.float .9
|
||||||
@@
|
@@
|
||||||
@@ -29,14 +35,6 @@
|
|||||||
#:python_cast (lambda s : [__import__("kutil").parse_length("tex", e) for e in s.split()])
|
#:python_cast (lambda s : [__import__("kutil").parse_length("tex", e) for e in s.split()])
|
||||||
@@@
|
@@@
|
||||||
|
|
||||||
@@@argtype side | a side of a box
|
|
||||||
:pattern top^|right^|bottom^|left
|
|
||||||
@@@
|
|
||||||
|
|
||||||
@@@argtype hpos | horizontal position
|
|
||||||
:pattern left^|center^|right^|none
|
|
||||||
@@@
|
|
||||||
|
|
||||||
@@@argtype figure_id |
|
@@@argtype figure_id |
|
||||||
an identifier for a figure.
|
an identifier for a figure.
|
||||||
|
|
||||||
|
|||||||
@@ -91,20 +91,32 @@ def caption_marker(name, caption, delimiter=" - "):
|
|||||||
caption = f"{delimiter}{caption}"
|
caption = f"{delimiter}{caption}"
|
||||||
return f"{d}{name}{d}{caption}{d}"
|
return f"{d}{name}{d}{caption}{d}"
|
||||||
|
|
||||||
|
def rest_split(s, dimensions=1):
|
||||||
|
"""Split bar-delimited text into nested lists, one level per dimension.
|
||||||
|
|
||||||
|
The delimiter for dimension n is a run of exactly n bar characters:
|
||||||
|
| separates elements, || separates lists of elements, ||| lists of
|
||||||
|
lists, and so on. This is the cast behind the rest(N) argument type.
|
||||||
|
One trailing top-level delimiter (the customary dangling separator
|
||||||
|
before a closing @) is removed; all other empty elements are
|
||||||
|
preserved, so a trailing | still makes an empty final cell.
|
||||||
|
"""
|
||||||
|
s = s.strip()
|
||||||
|
if not s:
|
||||||
|
return [] if dimensions > 0 else s
|
||||||
|
delimiter = "|" * dimensions
|
||||||
|
if s.endswith(delimiter) and not s.endswith("|" + delimiter):
|
||||||
|
s = s[:-len(delimiter)]
|
||||||
|
return _rest_split_level(s, dimensions)
|
||||||
|
|
||||||
|
def _rest_split_level(s, dimensions):
|
||||||
|
if dimensions <= 0:
|
||||||
|
return s.strip()
|
||||||
|
pattern = re.compile("(?<!\\|)" + "\\|" * dimensions + "(?!\\|)")
|
||||||
|
return [_rest_split_level(part, dimensions - 1) for part in pattern.split(s)]
|
||||||
|
|
||||||
def rest_args(s, dimensions=1):
|
def rest_args(s, dimensions=1):
|
||||||
if s.endswith("||"):
|
return rest_split(s, dimensions)
|
||||||
s = s[:-2]
|
|
||||||
s = re.sub(r"\t", r"\\t", s)
|
|
||||||
double_bar_pat = re.compile(r"\|\|")
|
|
||||||
bar_pat = re.compile(r"\s*\|\s*")
|
|
||||||
parts = [e.strip() for e in double_bar_pat.split(s.strip())]
|
|
||||||
for p in parts:
|
|
||||||
elts = bar_pat.split(p.strip())
|
|
||||||
elts = ["~" if (e.strip() == "") else e.strip() for e in elts]
|
|
||||||
result = [bar_pat.split(e) for e in parts]
|
|
||||||
if dimensions == 1:
|
|
||||||
result = result[0]
|
|
||||||
return result
|
|
||||||
|
|
||||||
def parse_length(target, s, rel_fraction):
|
def parse_length(target, s, rel_fraction):
|
||||||
def choose(html_value, tex_value):
|
def choose(html_value, tex_value):
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class Link(klammer_base.Klammer_base):
|
|||||||
# self.show("Link")
|
# self.show("Link")
|
||||||
|
|
||||||
def html(self):
|
def html(self):
|
||||||
return html_link(self.target, self.text, self.section == "true", self.nq)
|
return html_link(self.target, self.text, self.section, self.nq)
|
||||||
|
|
||||||
def tex(self):
|
def tex(self):
|
||||||
return latex_link(self.target, self.text, self.section, self.nq, self.footnote)
|
return latex_link(self.target, self.text, self.section, self.nq, self.footnote)
|
||||||
|
|||||||
@@ -9,9 +9,9 @@
|
|||||||
@@ol.k list_items.rest :cmp.bool false :initial.int 1 : Ordered list, with numbered list elements. @@
|
@@ol.k list_items.rest :cmp.bool false :initial.int 1 : Ordered list, with numbered list elements. @@
|
||||||
@@ol :: @eval list.List(K, "ol") eval@ @@
|
@@ol :: @eval list.List(K, "ol") eval@ @@
|
||||||
|
|
||||||
@@define descriptions.rest :font i : @eval list.Define(K) eval@ @@
|
@@define descriptions.rest(2) :font i : @eval list.Define(K) eval@ @@
|
||||||
|
|
||||||
@@columns items.rest :n.int 2 : @eval list.columns(K) eval@ @@
|
@@columns items :n.int 2 : @eval list.columns(K) eval@ @@
|
||||||
##
|
##
|
||||||
|
|
||||||
#[
|
#[
|
||||||
|
|||||||
@@ -11,9 +11,7 @@ class List(klammer_base.Klammer_base):
|
|||||||
def __init__(self, K, list_type):
|
def __init__(self, K, list_type):
|
||||||
super().__init__(K)
|
super().__init__(K)
|
||||||
self.list_type = list_type
|
self.list_type = list_type
|
||||||
self.items = kutil.rest_args(self.list_items)
|
self.items = self.list_items
|
||||||
if self.items[-1].strip() == "":
|
|
||||||
self.items = self.items[:-1]
|
|
||||||
word_count = max([len(e.strip().split()) for e in self.items])
|
word_count = max([len(e.strip().split()) for e in self.items])
|
||||||
self.compressed = False #and word_count < int(self.List_compressed_word_max)
|
self.compressed = False #and word_count < int(self.List_compressed_word_max)
|
||||||
|
|
||||||
@@ -31,11 +29,11 @@ class List(klammer_base.Klammer_base):
|
|||||||
command = {'ol' : 'enumerate', 'ul' : 'itemize'}[self.list_type]
|
command = {'ol' : 'enumerate', 'ul' : 'itemize'}[self.list_type]
|
||||||
init = ""
|
init = ""
|
||||||
if self.__dict__.get("initial") and self.initial != 1:
|
if self.__dict__.get("initial") and self.initial != 1:
|
||||||
init = "\\addtocounter{enumi}{" + str(int(self.initial) - 1) + "}"
|
init = "\\addtocounter{enumi}{" + str(self.initial - 1) + "}"
|
||||||
#topsep = '[noitemsep,topsep=0pt]'
|
#topsep = '[noitemsep,topsep=0pt]'
|
||||||
topsep = "[noitemsep,topsep=0pt," if self.compressed else "[topsep=0pt,itemsep=2pt,"
|
topsep = "[noitemsep,topsep=0pt," if self.compressed else "[topsep=0pt,itemsep=2pt,"
|
||||||
topsep += "leftmargin=16pt]"
|
topsep += "leftmargin=16pt]"
|
||||||
listsep = '\\setlist{nolistsep}' if self.cmp == 'true' else ''
|
listsep = '\\setlist{nolistsep}' if self.cmp else ''
|
||||||
result = f'{listsep}\n\\begin{{{command}}}{topsep}{init}\n{body}\n\\end{{{command}}}\n'
|
result = f'{listsep}\n\\begin{{{command}}}{topsep}{init}\n{body}\n\\end{{{command}}}\n'
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -58,7 +56,7 @@ class List(klammer_base.Klammer_base):
|
|||||||
class Define(klammer_base.Klammer_base):
|
class Define(klammer_base.Klammer_base):
|
||||||
def __init__(self, K):
|
def __init__(self, K):
|
||||||
super().__init__(K)
|
super().__init__(K)
|
||||||
self.items = kutil.rest_args(self.descriptions, 2)
|
self.items = self.descriptions
|
||||||
#self.show()
|
#self.show()
|
||||||
#pprint.pprint(self.items)
|
#pprint.pprint(self.items)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
include $(KLAMMERTEXT_HOME)/mac/env/makefile.env
|
include $(KLAMMERTEXT_HOME)/env/makefile.env
|
||||||
|
|
||||||
FLAGS = -std=c++17 -fvisibility=hidden -I $(KLAMMERTEXT_HOME)/src -I$(KLAMMERTEXT_HOME)/sks/kutil $(CXXFLAGS)
|
FLAGS = -std=c++17 -fvisibility=hidden -I $(KLAMMERTEXT_HOME)/src -I$(KLAMMERTEXT_HOME)/sks/kutil $(CXXFLAGS)
|
||||||
|
|
||||||
|
|||||||
197
sks/table/indexed_range.py
Normal file
197
sks/table/indexed_range.py
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
"""Parsing for the indexed_range argument syntax.
|
||||||
|
|
||||||
|
An indexed_range selects positions in one dimension of a grid, with an
|
||||||
|
optional extent in the other dimension. The same syntax serves the table
|
||||||
|
klammer's :hline and :vline arguments (index = boundary, subsets = how far
|
||||||
|
along the line) and its :colspan and :rowspan arguments (index = row or
|
||||||
|
column, subsets = the cells to merge). Which dimension the index selects
|
||||||
|
is a property of the argument, not of the syntax.
|
||||||
|
|
||||||
|
This module replaces the former sequences.py and span.py (see debris).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
syntax_description = """
|
||||||
|
An indexed_range is a selector, optionally followed by parenthesized
|
||||||
|
subsets, written with no spaces:
|
||||||
|
|
||||||
|
<selector> full extent
|
||||||
|
<selector>(<subsets>) restricted extent
|
||||||
|
|
||||||
|
The selector is a single index "3", a closed index range "2-5", an open
|
||||||
|
index range "2-" (to the last index), or a name defined by the argument
|
||||||
|
(for example "top" or "inner" for table lines). Subsets are separated by
|
||||||
|
commas; each is an index "4", a closed range "1-4", or an open range "6-"
|
||||||
|
(to the end). All indices are zero-origin.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
3 index 3, full extent
|
||||||
|
2-5(0-2) indices 2 through 5, each restricted to 0 through 2
|
||||||
|
3(1-4,6-9) index 3, restricted to 1-4 and 6-9
|
||||||
|
head(1-) with table hline names: boundary 1, from column 1 on
|
||||||
|
""".strip()
|
||||||
|
|
||||||
|
|
||||||
|
class Range_error(Exception):
|
||||||
|
def __init__(self, message):
|
||||||
|
super().__init__(f"{message}\n\n{syntax_description}")
|
||||||
|
|
||||||
|
|
||||||
|
item_rgx = re.compile(r"(?:(\d+)(-)?(\d*)|([A-Za-z]+))(?:\(([\d,\-]+)\))?$")
|
||||||
|
subset_rgx = re.compile(r"(\d+)(-)?(\d*)$")
|
||||||
|
|
||||||
|
|
||||||
|
def hline_names(count):
|
||||||
|
"""Boundary-name map for horizontal lines; count = row_count + 1."""
|
||||||
|
last = count - 1
|
||||||
|
return {"top": [0],
|
||||||
|
"head": [1],
|
||||||
|
"bottom": [last],
|
||||||
|
"inner": list(range(1, last)),
|
||||||
|
"all": list(range(count))}
|
||||||
|
|
||||||
|
|
||||||
|
def vline_names(count):
|
||||||
|
"""Boundary-name map for vertical lines; count = column_count + 1."""
|
||||||
|
last = count - 1
|
||||||
|
return {"outer": [0, last],
|
||||||
|
"inner": list(range(1, last)),
|
||||||
|
"all": list(range(count))}
|
||||||
|
|
||||||
|
|
||||||
|
class Indexed_range:
|
||||||
|
"""The selected extent for one primary-dimension index."""
|
||||||
|
|
||||||
|
def __init__(self, index, maxval):
|
||||||
|
self.index = index
|
||||||
|
self.maxval = maxval
|
||||||
|
self.all = False # Full extent (no subsets given)
|
||||||
|
self.ranges = [] # [[start, end], ...], inclusive
|
||||||
|
|
||||||
|
def add_full(self):
|
||||||
|
self.all = True
|
||||||
|
self.ranges = [[0, self.maxval]]
|
||||||
|
|
||||||
|
def add_ranges(self, ranges):
|
||||||
|
if not self.all:
|
||||||
|
self.ranges += ranges
|
||||||
|
|
||||||
|
def has(self, i):
|
||||||
|
return any(start <= i <= end for start, end in self.ranges)
|
||||||
|
|
||||||
|
def items(self, invert=False):
|
||||||
|
result = []
|
||||||
|
for start, end in self.ranges:
|
||||||
|
for e in range(start, end + 1):
|
||||||
|
result.append((e, self.index) if invert else (self.index, e))
|
||||||
|
return result
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
subsets = ",".join([f"{s}-{e}" for s, e in self.ranges])
|
||||||
|
return f"{self.index}({subsets})"
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return self.__str__()
|
||||||
|
|
||||||
|
|
||||||
|
class Indexed_ranges:
|
||||||
|
"""A parsed indexed_range argument: Indexed_range entries by index.
|
||||||
|
|
||||||
|
count - number of valid primary indices (0 .. count-1)
|
||||||
|
maxval - largest valid subset value (the cross dimension)
|
||||||
|
specs - the argument value: a list of items (from the argtype's
|
||||||
|
python_cast), a whitespace-separated string, or None
|
||||||
|
names - map of selector names to index lists (hline_names, ...)
|
||||||
|
argument - argument name for error messages (":hline", ...)
|
||||||
|
|
||||||
|
Items targeting the same index merge: their subsets are unioned, and a
|
||||||
|
full-extent item absorbs any subsets.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, count, maxval, specs, names=None, argument=""):
|
||||||
|
self.count = count
|
||||||
|
self.maxval = maxval
|
||||||
|
self.names = names or {}
|
||||||
|
self.argument = argument
|
||||||
|
self.by_index = {}
|
||||||
|
if specs is None:
|
||||||
|
specs = []
|
||||||
|
elif isinstance(specs, str):
|
||||||
|
specs = specs.split()
|
||||||
|
for spec in specs:
|
||||||
|
self.parse(spec)
|
||||||
|
|
||||||
|
def error(self, message):
|
||||||
|
argument = f"{self.argument} argument: " if self.argument else ""
|
||||||
|
raise Range_error(f"{argument}{message}")
|
||||||
|
|
||||||
|
def parse(self, spec):
|
||||||
|
match = item_rgx.match(spec)
|
||||||
|
if not match:
|
||||||
|
self.error(f'"{spec}" is not a valid indexed_range.')
|
||||||
|
number, hyphen, end, name, subsets = match.groups()
|
||||||
|
if name is not None:
|
||||||
|
if name not in self.names:
|
||||||
|
known = " ".join(self.names) or "none"
|
||||||
|
self.error(f'"{name}" is not a valid name here '
|
||||||
|
f"(valid names: {known}).")
|
||||||
|
indices = self.names[name]
|
||||||
|
else:
|
||||||
|
start = int(number)
|
||||||
|
if not hyphen:
|
||||||
|
indices = [start]
|
||||||
|
else:
|
||||||
|
last = int(end) if end else self.count - 1
|
||||||
|
if start > last:
|
||||||
|
self.error(f'In "{spec}", the index range start {start} '
|
||||||
|
f"is greater than its end {last}.")
|
||||||
|
indices = list(range(start, last + 1))
|
||||||
|
for i in indices:
|
||||||
|
if i >= self.count:
|
||||||
|
self.error(f'In "{spec}", index {i} is out of range '
|
||||||
|
f"(0 through {self.count - 1}).")
|
||||||
|
ranges = self.parse_subsets(spec, subsets) if subsets else None
|
||||||
|
for i in indices:
|
||||||
|
entry = self.by_index.setdefault(i, Indexed_range(i, self.maxval))
|
||||||
|
if ranges is None:
|
||||||
|
entry.add_full()
|
||||||
|
else:
|
||||||
|
entry.add_ranges(ranges)
|
||||||
|
|
||||||
|
def parse_subsets(self, spec, subsets):
|
||||||
|
ranges = []
|
||||||
|
for part in subsets.split(","):
|
||||||
|
match = subset_rgx.match(part)
|
||||||
|
if not match:
|
||||||
|
self.error(f'In "{spec}", "{part}" is not a valid subset.')
|
||||||
|
number, hyphen, end = match.groups()
|
||||||
|
start = int(number)
|
||||||
|
if not hyphen:
|
||||||
|
last = start
|
||||||
|
else:
|
||||||
|
last = int(end) if end else self.maxval
|
||||||
|
if start > last:
|
||||||
|
self.error(f'In "{spec}", the subset start {start} '
|
||||||
|
f"is greater than its end {last}.")
|
||||||
|
if last > self.maxval:
|
||||||
|
self.error(f'In "{spec}", {last} is out of range '
|
||||||
|
f"(0 through {self.maxval}).")
|
||||||
|
ranges.append([start, last])
|
||||||
|
return ranges
|
||||||
|
|
||||||
|
def __getitem__(self, index):
|
||||||
|
return self.by_index.get(index)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(self.by_index)
|
||||||
|
|
||||||
|
def has(self, index, i):
|
||||||
|
entry = self[index]
|
||||||
|
return entry.has(i) if entry else False
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return " ".join([str(self.by_index[i]) for i in sorted(self.by_index)])
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return self.__str__()
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
import sys
|
|
||||||
import re
|
|
||||||
|
|
||||||
syntax_description = """
|
|
||||||
|
|
||||||
A "sequence" is an integer (the "index") followed by an optional description of one
|
|
||||||
or more sequence subsets. A subset is defined by a series of subset
|
|
||||||
descriptions, separated by a comma. A subset description is either an integer,
|
|
||||||
two integers separated by a hypen to indicate a range, or an integer followed
|
|
||||||
only by a hyphen, which will include all the following elements of the sequence to
|
|
||||||
the end. No spaces are allowed in a sequence. All indices are zero-origin.
|
|
||||||
|
|
||||||
Sequence examples for an index of "3":
|
|
||||||
3
|
|
||||||
3(1)
|
|
||||||
3(0-4)
|
|
||||||
3(1-4,6-9)
|
|
||||||
3(5-)
|
|
||||||
|
|
||||||
Note that some sequence subsets must include two numbers, for example, border
|
|
||||||
lines in a table.
|
|
||||||
|
|
||||||
""".strip()
|
|
||||||
|
|
||||||
|
|
||||||
class Sequence:
|
|
||||||
def __init__(self, count, maxval, spec):
|
|
||||||
def parse_range(match):
|
|
||||||
start, hyphen, end = match.groups()
|
|
||||||
if hyphen is None and end is None:
|
|
||||||
end = start
|
|
||||||
elif end is None:
|
|
||||||
end = maxval
|
|
||||||
return [int(start), int(end)]
|
|
||||||
self.maxval = maxval
|
|
||||||
self.all = True
|
|
||||||
subset_pat = "[-\\d,]+"
|
|
||||||
sequence_rgx = re.compile(fr"(\d+)(\({subset_pat}\))*")
|
|
||||||
match = sequence_rgx.match(spec)
|
|
||||||
self.spec = spec
|
|
||||||
if match and match.group(0) == spec:
|
|
||||||
range_rgx = re.compile(r"(\d+)(-)?(\d+)?")
|
|
||||||
self.index = int(match.group(1))
|
|
||||||
if (match.group(2)):
|
|
||||||
self.subsets = match.group(2).strip("()").split(",")
|
|
||||||
matches = [range_rgx.match(e) for e in self.subsets]
|
|
||||||
self.ranges = [parse_range(e) if e else None for e in matches]
|
|
||||||
self.all = False
|
|
||||||
else:
|
|
||||||
self.ranges = [[0, self.maxval]]
|
|
||||||
else:
|
|
||||||
print(f'The sequence specification "{spec}" is incorrect.\n\n{syntax_description}\n')
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
#subsets = "all" if self.all else ",".join([f"{e[0]}-{e[1]}" for e in self.ranges])
|
|
||||||
subsets = "all" if False else ",".join([f"{e[0]}-{e[1]}" for e in self.ranges])
|
|
||||||
return f"{self.index}[{subsets}]"
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return self.__str__()
|
|
||||||
|
|
||||||
def has(self, i):
|
|
||||||
for start, end in self.ranges:
|
|
||||||
if i >= start and i <= end:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def items(self, invert=False):
|
|
||||||
result = []
|
|
||||||
for start,end in self.ranges:
|
|
||||||
for e in range(start, end+1):
|
|
||||||
result.append((e, self.index) if invert else (self.index, e))
|
|
||||||
return result
|
|
||||||
|
|
||||||
class Sequences:
|
|
||||||
def __init__(self, count, maxval, sequence_specs):
|
|
||||||
self.count = count
|
|
||||||
self.maxval = maxval
|
|
||||||
self.sequences = {}
|
|
||||||
match_all = re.compile(r"\*(.*)").match(sequence_specs)
|
|
||||||
match_some = re.compile(r"\[(\d+)-(\d+)\](.*)").match(sequence_specs)
|
|
||||||
spec_list = []
|
|
||||||
if match_all:
|
|
||||||
subseq = match_all.group(1)
|
|
||||||
for i in range(count):
|
|
||||||
spec_list.append(f"{i}{subseq}")
|
|
||||||
elif match_some:
|
|
||||||
start = int(match_some.group(1))
|
|
||||||
end = int(match_some.group(2))
|
|
||||||
subseq = match_some.group(3)
|
|
||||||
for i in range(start, end+1):
|
|
||||||
spec_list.append(f"{i}{subseq}")
|
|
||||||
else:
|
|
||||||
spec_list = sequence_specs.split()
|
|
||||||
for specs in spec_list:
|
|
||||||
for spec in self.parse_spec(specs):
|
|
||||||
self.sequences[spec.index] = spec
|
|
||||||
|
|
||||||
def __getitem__(self, index):
|
|
||||||
return self.sequences.get(index)
|
|
||||||
|
|
||||||
def has(self, index, subseq_index):
|
|
||||||
return self[index].has(subseq_index) if self[index] else None
|
|
||||||
|
|
||||||
def parse_spec(self, spec):
|
|
||||||
named_spec = { "last" : [str(self.count-1)],
|
|
||||||
"outer" : ["0", str(self.count-1)],
|
|
||||||
"inner" : [str(e) for e in range(1, self.count - 1)],
|
|
||||||
"all" : [str(e) for e in range(0, self.count)]
|
|
||||||
}.get(spec)
|
|
||||||
if named_spec is None:
|
|
||||||
return [Sequence(self.count, self.maxval, spec)]
|
|
||||||
else:
|
|
||||||
return [Sequence(self.count, self.maxval, e) for e in named_spec]
|
|
||||||
|
|
||||||
def items(self, invert=False):
|
|
||||||
result = []
|
|
||||||
for seq in self.sequences:
|
|
||||||
result += self.sequences[seq].items(invert)
|
|
||||||
return set(result)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
import pprint
|
|
||||||
for s in [
|
|
||||||
Sequence(10, "0"),
|
|
||||||
Sequence(10, "1"),
|
|
||||||
Sequence(10, "2(2)"),
|
|
||||||
Sequence(10, "3(2-)"),
|
|
||||||
Sequence(10, "4(2-4)"),
|
|
||||||
Sequence(10, "5(2-4)"),
|
|
||||||
Sequence(10, "6(2-4,6)"),
|
|
||||||
Sequence(10, "7(2,6-8)"),
|
|
||||||
Sequence(10, "8(2,7-8,9-11,13-14)")]:
|
|
||||||
print(s.spec, "->", s)
|
|
||||||
|
|
||||||
print("Sequences")
|
|
||||||
S = Sequences(10, "0")
|
|
||||||
for name in "top bottom head outer inner all 1(2-3)".split():
|
|
||||||
print(name, "->", S.parse_spec(name))
|
|
||||||
|
|
||||||
print("Instantiate:")
|
|
||||||
s = Sequences(10, "3(1-2,4-5) 4(8-9)")
|
|
||||||
|
|
||||||
#print(s.items(True))
|
|
||||||
print(s)
|
|
||||||
print(s.has(3,1))
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
import sys
|
|
||||||
import re
|
|
||||||
|
|
||||||
syntax_description = """
|
|
||||||
|
|
||||||
A "span" is an integer (the "index") followed by an optional description of one
|
|
||||||
or more sequence subsets. A subset is defined by a series of subset
|
|
||||||
descriptions, separated by a comma. A subset description is either an integer,
|
|
||||||
two integers separated by a hypen to indicate a range, or an integer followed
|
|
||||||
only by a hyphen, which will include all the following elements of the span to
|
|
||||||
the end. No spaces are allowed in a span. All indices are zero-origin.
|
|
||||||
|
|
||||||
Span examples for an index of "3":
|
|
||||||
3
|
|
||||||
3(1)
|
|
||||||
3(0-4)
|
|
||||||
3(1-4,6-9)
|
|
||||||
3(5-)
|
|
||||||
|
|
||||||
Note that some sequence subsets must include two numbers, for example, border
|
|
||||||
lines in a table.
|
|
||||||
|
|
||||||
""".strip()
|
|
||||||
|
|
||||||
|
|
||||||
class Span:
|
|
||||||
def __init__(self, count, spec):
|
|
||||||
def parse_range(match):
|
|
||||||
start, hyphen, end = match.groups()
|
|
||||||
if hyphen is None and end is None:
|
|
||||||
end = start
|
|
||||||
elif end is None:
|
|
||||||
end = count - 1
|
|
||||||
return [int(start), int(end)]
|
|
||||||
self.all = True
|
|
||||||
subset_pat = "[-\\d,]+"
|
|
||||||
span_rgx = re.compile(f"(\d+)(\({subset_pat}\))*")
|
|
||||||
match = span_rgx.match(spec)
|
|
||||||
self.spec = spec
|
|
||||||
if match and match.group(0) == spec:
|
|
||||||
range_rgx = re.compile("(\d+)(-)?(\d+)?")
|
|
||||||
self.index = int(match.group(1))
|
|
||||||
if (match.group(2)):
|
|
||||||
self.subsets = match.group(2).strip("()").split(",")
|
|
||||||
matches = [range_rgx.match(e) for e in self.subsets]
|
|
||||||
self.ranges = [parse_range(e) if e else None for e in matches]
|
|
||||||
self.all = False
|
|
||||||
else:
|
|
||||||
self.ranges = [[0, count-1]]
|
|
||||||
else:
|
|
||||||
print(f'The span specification "{spec}" is incorrect.\n\n{syntax_description}\n')
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
subsets = "all" if self.all else ",".join([f"{e[0]}-{e[1]}" for e in self.ranges])
|
|
||||||
return f"{self.index}[{subsets}]"
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return self.__str__()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Spanset:
|
|
||||||
def __init__(self, count, span_specs):
|
|
||||||
self.count = count
|
|
||||||
self.spans = {}
|
|
||||||
for specs in span_specs.split():
|
|
||||||
for spec in self.parse_spec(specs):
|
|
||||||
print("Spanset spec:", spec)
|
|
||||||
self.spans[spec.index] = spec
|
|
||||||
|
|
||||||
|
|
||||||
def parse_spec(self, spec):
|
|
||||||
named_spec = {"top" : ["0"],
|
|
||||||
"bottom" : [str(self.count-1)],
|
|
||||||
"head" : ["1"],
|
|
||||||
"outer" : ["0", str(self.count-1)],
|
|
||||||
"inner" : [str(e) for e in range(1,self.count-1)],
|
|
||||||
"all" : [str(e) for e in range(0,self.count+1)]
|
|
||||||
}.get(spec)
|
|
||||||
if named_spec is None:
|
|
||||||
return [Span(self.count, spec)]
|
|
||||||
else:
|
|
||||||
return [Span(self.count, e) for e in named_spec]
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
import pprint
|
|
||||||
for s in [
|
|
||||||
Span(10, "0"),
|
|
||||||
Span(10, "1"),
|
|
||||||
Span(10, "2(2)"),
|
|
||||||
Span(10, "3(2-)"),
|
|
||||||
Span(10, "4(2-4)"),
|
|
||||||
Span(10, "5(2-4)"),
|
|
||||||
Span(10, "6(2-4,6)"),
|
|
||||||
Span(10, "7(2,6-8)"),
|
|
||||||
Span(10, "8(2,7-8,9-11,13-14)")]:
|
|
||||||
print(s.spec, "->", s)
|
|
||||||
|
|
||||||
print("Spanset")
|
|
||||||
S = Spanset(10, "1")
|
|
||||||
for name in "top bottom head outer inner all 1(2-3)".split():
|
|
||||||
print(name, "->", S.parse_spec(name))
|
|
||||||
|
|
||||||
print("Instantiate:")
|
|
||||||
s = Spanset(10, "3(1-2,4-5)")
|
|
||||||
print(s.tex_hline(3))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,63 +1,165 @@
|
|||||||
#[ f: 0->1; n: 1.. (integer)
|
# Table argument types and klammer declaration
|
||||||
l, c, r
|
|
||||||
[<numeric-size>] <width> <justification>
|
|
||||||
width is widest line in a cell f [default]
|
|
||||||
width is fraction of table <f>t
|
|
||||||
width is specific length <n>pt|px|in|cm [...not portable?]
|
|
||||||
width width is remaining (evenly divided) *
|
|
||||||
]#
|
|
||||||
|
|
||||||
@@@argtype table_hpos |
|
@@@argtype index_subsets |
|
||||||
|
one or more subsets in parentheses, attached to an index. Each subset is a
|
||||||
|
single index <n>, a closed range <n>-<m>, or an open range <n>- (from <n> to
|
||||||
|
the end). Several subsets are separated by commas, with no spaces.
|
||||||
|
Example: (1-4,6-9)
|
||||||
|
:pattern \((?^:\d+(?^:-\d*)?)(?^:,\d+(?^:-\d*)?)*\)
|
||||||
|
@@@
|
||||||
|
|
||||||
|
@@@argtype indexed_range |
|
||||||
|
an index with optional subsets, written with no spaces. The index part is a
|
||||||
|
single index <i>, a closed index range <i>-<j>, or an open index range <i>-
|
||||||
|
(from <i> to the last index). It may be followed by parenthesized subsets
|
||||||
|
(see the index_subsets type) restricting the extent in the other dimension.
|
||||||
|
All indices are zero-origin. Examples:
|
||||||
|
|
||||||
|
3 index 3, full extent
|
||||||
|
2-5 indices 2 through 5, full extent
|
||||||
|
3(1-4,6-9) index 3, restricted to 1 through 4 and 6 through 9
|
||||||
|
2-5(0-2) indices 2 through 5, each restricted to 0 through 2
|
||||||
|
|
||||||
|
:pattern \d+(?^:-\d*)?(?^:'index_subsets')?
|
||||||
|
@@@
|
||||||
|
|
||||||
|
@@@argtype column_width |
|
||||||
|
width of the table columns. Each column is one of 'fit' (widest line of the
|
||||||
|
cells in that column), a fraction 0.0->1.0 (that fraction of the total table
|
||||||
|
width), or '*' (use the remaining width of the table; there can only be one
|
||||||
|
column with '*'). If there are fewer positions than columns in the table, the
|
||||||
|
last value is repeated. Extra positions generate a warning.
|
||||||
|
:pattern (fit^|f^|0?\.\d+^|\*^|\s+)+
|
||||||
|
:python_cast (lambda s : s.split())
|
||||||
|
:default fit
|
||||||
|
@@@
|
||||||
|
|
||||||
|
@@@argtype cell_hpos |
|
||||||
horizontal formatting in a table cell. One of 'l', 'c' or 'r' for each
|
horizontal formatting in a table cell. One of 'l', 'c' or 'r' for each
|
||||||
cell in a row. If there are fewer positions than cells in a row, the
|
cell in a row. If there are fewer positions than cells in a row, the
|
||||||
last value is repeated. Extra positions generate a warning. Default is 'l'
|
last value is repeated. Extra positions generate a warning.
|
||||||
# :pattern ((f^|(0?\.'uint't^|\\*))[lcr]?^|\s)+
|
:pattern (l^|c^|r^|\s+)+
|
||||||
# :pattern ((f^|'float't^|\\*)[lcr]?^|\s)+
|
|
||||||
:pattern ([.\w]+^|\*^|\s+)+
|
|
||||||
#:pattern (l^|c^|r^|\s)+
|
|
||||||
:python_cast (lambda s : s.split())
|
:python_cast (lambda s : s.split())
|
||||||
|
:default l
|
||||||
@@@
|
@@@
|
||||||
|
|
||||||
@@@argtype table_hline |
|
@@@argtype table_hline |
|
||||||
a table's horizontal line description; one or more of 'top',
|
|
||||||
'head', 'inner', 'bottom', or a row number for a line at the bottom
|
a table's horizontal lines, as one or more whitespace-separated items.
|
||||||
of that row
|
With N rows there are N+1 horizontal boundaries, numbered 0 to N from the
|
||||||
#:pattern (top^|head^|inner^|bottom^|\d+^|\d+:\(\d+\-\d+\)^|\s+)*
|
top; boundary i lies above row i, and boundary N is the bottom. An item
|
||||||
#:python_cast (lambda s : s.split())
|
is either a boundary name or an indexed_range of boundary indices. The
|
||||||
|
names are 'top' (boundary 0), 'head' (boundary 1, under a header row),
|
||||||
|
'bottom' (boundary N), 'inner' (all boundaries between top and bottom),
|
||||||
|
and 'all' (every boundary). A name or index may be followed by
|
||||||
|
parenthesized subsets to draw only part of a line, given as zero-origin
|
||||||
|
column ranges. Examples:
|
||||||
|
|
||||||
|
top bottom lines above and below the table
|
||||||
|
head(1-) a line under the header, from column 1 to the last
|
||||||
|
3(1-4,6-9) two partial lines at boundary 3
|
||||||
|
all every line
|
||||||
|
|
||||||
|
:pattern ((?^:top^|head^|inner^|bottom^|all)(?^:'index_subsets')?^|'indexed_range'^|\s+)+
|
||||||
|
:python_cast (lambda s : s.split())
|
||||||
@@@
|
@@@
|
||||||
|
|
||||||
@@@argtype table_vline |
|
@@@argtype table_vline |
|
||||||
a table's vertical line description; one or more of 'outer', 'inner,
|
|
||||||
or a column number for a line at the right of that column
|
a table's vertical lines, as one or more whitespace-separated items.
|
||||||
# :pattern (outer^|inner^|\d+^|\d+^|\d+:\(\d+\-\d+\)^|\s+)*
|
With M columns there are M+1 vertical boundaries, numbered 0 to M from
|
||||||
# :python_cast (lambda s : s.split())
|
the left; boundary i lies to the left of column i, and boundary M is the
|
||||||
|
right edge. An item is either a boundary name or an indexed_range of
|
||||||
|
boundary indices. The names are 'outer' (boundaries 0 and M), 'inner'
|
||||||
|
(all boundaries between them), and 'all' (every boundary). A name or
|
||||||
|
index may be followed by parenthesized subsets to draw only part of a
|
||||||
|
line, given as zero-origin row ranges. Examples:
|
||||||
|
|
||||||
|
outer lines at the left and right edges
|
||||||
|
2(0-3) a line left of column 2, spanning rows 0 through 3
|
||||||
|
all every line
|
||||||
|
|
||||||
|
:pattern ((?^:outer^|inner^|all)(?^:'index_subsets')?^|'indexed_range'^|\s+)+
|
||||||
|
:python_cast (lambda s : s.split())
|
||||||
@@@
|
@@@
|
||||||
|
|
||||||
@@@argtype table_span |
|
@@@argtype table_span |
|
||||||
a list of spans in a table in the form (X,Y):N (no spaces), where
|
|
||||||
(X,Y) is the position in the table (zero origin in the top left
|
a list of cell spans, each an indexed_range whose index selects the row
|
||||||
corner) and N is the number of columns or rows in the span
|
(for colspan) or the column (for rowspan), and whose parenthesized subset
|
||||||
#:pattern (\(\d+(?:-\d+)?,\d+(?:-\d+)?\):\d+\s*)*
|
gives the zero-origin range of cells to merge. An index range repeats
|
||||||
#:python_cast (lambda s : s.split())
|
the same span; several subsets make several spans. Examples for colspan:
|
||||||
|
|
||||||
|
1(2-4) in row 1, merge columns 2 through 4
|
||||||
|
1(0-1,3-5) two merges in row 1
|
||||||
|
2-4(0-1) the same merge in rows 2 through 4
|
||||||
|
|
||||||
|
:pattern ('indexed_range'^|\s+)+
|
||||||
|
:python_cast (lambda s : s.split())
|
||||||
|
@@@
|
||||||
|
|
||||||
|
@@@argtype table_calc |
|
||||||
|
|
||||||
|
calculations that fill table cells with computed values, separated by
|
||||||
|
semicolons. Each calculation has the form
|
||||||
|
|
||||||
|
<target> = <operator> <operand> <operand> ...
|
||||||
|
|
||||||
|
where the target is a single cell written <row>(<column>) with zero-origin
|
||||||
|
indices, the operator is one of + - * /, and each operand is either a cell
|
||||||
|
selection or a number. A cell selection is an indexed_range read as
|
||||||
|
<rows>(<columns>); a range expands to all of its cells in row order, so
|
||||||
|
"+ 1-2(3)" sums column 3 of rows 1 and 2. A plain number is a constant
|
||||||
|
and always uses a period as its decimal mark. Operators fold from the
|
||||||
|
left ("- 1(0-2)" is a minus b minus c); with a single operand, - negates
|
||||||
|
and / gives the reciprocal. Calculations run in the order given, and each
|
||||||
|
reads the values earlier calculations have written, as displayed.
|
||||||
|
Example:
|
||||||
|
|
||||||
|
1(3) = * 1(1-2) ;
|
||||||
|
2(3) = * 2(1-2) ;
|
||||||
|
3(3) = + 1-2(3)
|
||||||
|
|
||||||
|
:pattern \s*(\d+\(\d+\)\s*=\s*[-+*/](\s+(\d+(?^:-\d*)?'index_subsets'^|'float'))+\s*(;\s*^|\s*$))+
|
||||||
|
|
||||||
|
@@@
|
||||||
|
|
||||||
|
@@@argtype decimal_mark |
|
||||||
|
the character used as the decimal mark in numeric cell values, either
|
||||||
|
'period' (1,234.56) or 'comma' (1.234,56). Governs both the reading of
|
||||||
|
numbers from cells in table calculations and the formatting of
|
||||||
|
calculated values.
|
||||||
|
:pattern period^|comma
|
||||||
|
:default period
|
||||||
|
@@@
|
||||||
|
|
||||||
|
@@@argtype format_spec |
|
||||||
|
a Python format specification applied to calculated cell values, for
|
||||||
|
example ",.2f" for two decimal places with grouped thousands.
|
||||||
|
:pattern \S+
|
||||||
@@@
|
@@@
|
||||||
|
|
||||||
@@rowcolor.tex s : \colorrow{*s*} @@
|
@@rowcolor.tex s : \colorrow{*s*} @@
|
||||||
|
|
||||||
@@table rows.rest
|
@@table rows.rest(2)
|
||||||
:id
|
:id
|
||||||
@caption_arguments@
|
@caption_arguments@
|
||||||
:center.bool true
|
:center.bool true
|
||||||
:indent.length 1em
|
:indent.length 1em
|
||||||
:header.bool true
|
:header.bool true
|
||||||
:allow_break.bool false
|
:allow_break.bool false
|
||||||
|
:column_width.column_width
|
||||||
:hline.table_hline
|
:hline.table_hline
|
||||||
:vline.table_vline
|
:vline.table_vline
|
||||||
:grid.bool false
|
:grid.bool false
|
||||||
:cell_hpos.table_hpos c
|
:cell_hpos.cell_hpos
|
||||||
:header_font.font i
|
:header_font.font i
|
||||||
:font.font_list r
|
:font.font_list
|
||||||
:colspan.table_span
|
:colspan.table_span
|
||||||
:rowspan.table_span
|
:rowspan.table_span
|
||||||
|
:calc.table_calc
|
||||||
|
:calc_format.format_spec
|
||||||
|
:decimal.decimal_mark
|
||||||
:leading.float 1.3
|
:leading.float 1.3
|
||||||
:colsep 4pt
|
:colsep 4pt
|
||||||
:
|
:
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ import klammer_base
|
|||||||
import html_util
|
import html_util
|
||||||
from html_util import E
|
from html_util import E
|
||||||
import latex_util
|
import latex_util
|
||||||
from sequences import Sequences
|
from indexed_range import Indexed_ranges, hline_names, vline_names
|
||||||
import table_cell
|
import table_cell
|
||||||
import font
|
import font
|
||||||
|
|
||||||
def extend(lst, count, fill=None):
|
def extend(lst, count, fill=None):
|
||||||
if isinstance(lst, str):
|
if isinstance(lst, str):
|
||||||
lst = lst.strip().split("\\s+")
|
lst = lst.split()
|
||||||
if fill is None:
|
if fill is None:
|
||||||
fill = lst[-1] if lst else ""
|
fill = lst[-1] if lst else ""
|
||||||
return lst + ([fill] * (count - len(lst)))
|
return lst + ([fill] * (count - len(lst)))
|
||||||
@@ -44,34 +44,154 @@ class Table(klammer_base.Klammer_base):
|
|||||||
id = 0
|
id = 0
|
||||||
def __init__(self, K):
|
def __init__(self, K):
|
||||||
super().__init__(K)
|
super().__init__(K)
|
||||||
# pprint.pprint(self.__dict__)
|
|
||||||
if self.grid:
|
if self.grid:
|
||||||
self.vline = "all"
|
self.vline = ["all"]
|
||||||
self.hline = "all"
|
self.hline = ["all"]
|
||||||
self.number = self.number == "true"
|
|
||||||
self.rows = kutil.rest_args(self.rows, 2)
|
|
||||||
self.row_count = len(self.rows)
|
self.row_count = len(self.rows)
|
||||||
if self.header:
|
if self.header:
|
||||||
self.hline += f" 1 {self.row_count}"
|
self.hline += ["1", str(self.row_count)]
|
||||||
self.row_size = max([len(e) for e in self.rows])
|
self.row_size = max([len(e) for e in self.rows])
|
||||||
self.s_vline = Sequences(self.row_size + 1, self.row_count - 1, self.vline)
|
# Rows with fewer cells than the widest row are padded with empty
|
||||||
self.s_hline = Sequences(self.row_count + 1, self.row_size - 1, self.hline)
|
# cells (last-value duplication is for argument lists, not content).
|
||||||
self.s_rowspan = Sequences(self.row_size, self.row_count, self.rowspan)
|
self.rows = [row + [""] * (self.row_size - len(row)) for row in self.rows]
|
||||||
self.s_colspan = Sequences(self.row_count, self.row_size, self.colspan)
|
if self.calc:
|
||||||
self.cell_hpos = extend(parse_hpos(self.K_target, self.cell_hpos.split()), self.row_size)
|
self.calculate()
|
||||||
|
self.s_vline = Indexed_ranges(self.row_size + 1, self.row_count - 1, self.vline,
|
||||||
|
vline_names(self.row_size + 1), ":vline")
|
||||||
|
self.s_hline = Indexed_ranges(self.row_count + 1, self.row_size - 1, self.hline,
|
||||||
|
hline_names(self.row_count + 1), ":hline")
|
||||||
|
self.s_rowspan = Indexed_ranges(self.row_size, self.row_count - 1, self.rowspan,
|
||||||
|
argument=":rowspan")
|
||||||
|
self.s_colspan = Indexed_ranges(self.row_count, self.row_size - 1, self.colspan,
|
||||||
|
argument=":colspan")
|
||||||
|
self.cell_hpos = extend(parse_hpos(self.K_target, self.cell_hpos), self.row_size)
|
||||||
#self.cell_hpos = self.cell_hpos.split(";")
|
#self.cell_hpos = self.cell_hpos.split(";")
|
||||||
self.font = extend(self.font, self.row_size)
|
self.font = extend(self.font, self.row_size)
|
||||||
self.make_cells(self.rows)
|
self.make_cells(self.rows)
|
||||||
|
|
||||||
def span_count(self, span_seq, row_i, col_i):
|
# Calculated cell values (:calc). Calculations run in the order given;
|
||||||
result = 0
|
# each reads cell values as displayed (display-precision semantics), so
|
||||||
row_seq = span_seq[row_i]
|
# a printed total always equals the sum of the printed lines. Only
|
||||||
if row_seq:
|
# calculation targets are formatted (:calc_format); other cells keep
|
||||||
for range in row_seq.ranges:
|
# their authored text. Future operators to consider: min, max, mean,
|
||||||
if range[0] == col_i:
|
# and a per-calculation format override.
|
||||||
result = range[1] - range[0] + 1
|
|
||||||
|
calc_target_rgx = re.compile(r"(\d+)\((\d+)\)$")
|
||||||
|
calc_cell_rgx = re.compile(r"\d+(-\d*)?\(")
|
||||||
|
|
||||||
|
def calc_error(self, calc, message):
|
||||||
|
raise Exception(f'In the :calc calculation "{calc}": {message}')
|
||||||
|
|
||||||
|
def parse_number(self, text, ref, calc):
|
||||||
|
s = text.strip()
|
||||||
|
if self.decimal == "comma":
|
||||||
|
s = s.translate(str.maketrans(",.", ".,"))
|
||||||
|
s = s.replace(",", "") # Remove thousands separators
|
||||||
|
try:
|
||||||
|
return float(s)
|
||||||
|
except ValueError:
|
||||||
|
self.calc_error(
|
||||||
|
calc, f'the cell {ref} contains "{text.strip()}", '
|
||||||
|
"which is not a number")
|
||||||
|
|
||||||
|
def format_number(self, value, calc):
|
||||||
|
if self.calc_format:
|
||||||
|
try:
|
||||||
|
s = format(value, self.calc_format)
|
||||||
|
except ValueError:
|
||||||
|
self.calc_error(
|
||||||
|
calc, f'"{self.calc_format}" is not a valid '
|
||||||
|
"format specification")
|
||||||
|
elif value.is_integer():
|
||||||
|
s = str(int(value))
|
||||||
|
else:
|
||||||
|
s = str(value)
|
||||||
|
if self.decimal == "comma":
|
||||||
|
s = s.translate(str.maketrans(",.", ".,"))
|
||||||
|
return s
|
||||||
|
|
||||||
|
def operand_values(self, token, calc):
|
||||||
|
# A token with subsets is a cell selection; a bare number is a
|
||||||
|
# constant (always period-decimal, independent of :decimal).
|
||||||
|
if not self.calc_cell_rgx.match(token):
|
||||||
|
return [float(token)]
|
||||||
|
selection = Indexed_ranges(self.row_count, self.row_size - 1,
|
||||||
|
[token], argument=":calc")
|
||||||
|
values = []
|
||||||
|
for row_i in selection.by_index:
|
||||||
|
for _, col_i in selection.by_index[row_i].items():
|
||||||
|
values.append(self.parse_number(
|
||||||
|
self.rows[row_i][col_i], f"{row_i}({col_i})", calc))
|
||||||
|
return values
|
||||||
|
|
||||||
|
def apply_operator(self, op, values, calc):
|
||||||
|
if len(values) == 1: # Lisp-style unary - and /
|
||||||
|
return {"+": values[0], "*": values[0],
|
||||||
|
"-": -values[0], "/": 1 / values[0]}[op]
|
||||||
|
result = values[0]
|
||||||
|
for v in values[1:]: # Fold from the left
|
||||||
|
if op == "+":
|
||||||
|
result += v
|
||||||
|
elif op == "-":
|
||||||
|
result -= v
|
||||||
|
elif op == "*":
|
||||||
|
result *= v
|
||||||
|
else:
|
||||||
|
result /= v
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def calculate(self):
|
||||||
|
for calc in [c.strip() for c in self.calc.split(";") if c.strip()]:
|
||||||
|
target, eq, expression = calc.partition("=")
|
||||||
|
match = self.calc_target_rgx.match(target.strip())
|
||||||
|
if not eq or not match:
|
||||||
|
self.calc_error(calc, "the target must be a single cell "
|
||||||
|
"written <row>(<column>), followed by \"=\"")
|
||||||
|
row_i, col_i = int(match.group(1)), int(match.group(2))
|
||||||
|
if row_i >= self.row_count or col_i >= self.row_size:
|
||||||
|
self.calc_error(
|
||||||
|
calc, f"the target {target.strip()} is outside the "
|
||||||
|
f"table (rows 0-{self.row_count - 1}, "
|
||||||
|
f"columns 0-{self.row_size - 1})")
|
||||||
|
tokens = expression.split()
|
||||||
|
if not tokens or tokens[0] not in "+-*/" or len(tokens) < 2:
|
||||||
|
self.calc_error(calc, "the expression must be an operator "
|
||||||
|
"(+ - * /) followed by at least one operand")
|
||||||
|
values = []
|
||||||
|
for token in tokens[1:]:
|
||||||
|
values += self.operand_values(token, calc)
|
||||||
|
try:
|
||||||
|
result = self.apply_operator(tokens[0], values, calc)
|
||||||
|
except ZeroDivisionError:
|
||||||
|
self.calc_error(calc, "division by zero")
|
||||||
|
self.rows[row_i][col_i] = self.format_number(result, calc)
|
||||||
|
|
||||||
|
def span_count(self, spans, index, cross_i):
|
||||||
|
# The count of cells merged by a span anchored at (index, cross_i):
|
||||||
|
# for colspan, index is the row and cross_i the column; for rowspan,
|
||||||
|
# index is the column and cross_i the row. Non-anchor cells get 0.
|
||||||
|
result = 0
|
||||||
|
entry = spans[index]
|
||||||
|
if entry:
|
||||||
|
for start, end in entry.ranges:
|
||||||
|
if start == cross_i:
|
||||||
|
result = end - start + 1
|
||||||
|
return result
|
||||||
|
|
||||||
|
def compute_coverage(self):
|
||||||
|
# Cells hidden by a span (every spanned cell except the anchor).
|
||||||
|
self.colspan_covered = set()
|
||||||
|
self.rowspan_covered = set()
|
||||||
|
for row_i in self.s_colspan.by_index:
|
||||||
|
for start, end in self.s_colspan.by_index[row_i].ranges:
|
||||||
|
for col_i in range(start + 1, end + 1):
|
||||||
|
self.colspan_covered.add((row_i, col_i))
|
||||||
|
for col_i in self.s_rowspan.by_index:
|
||||||
|
for start, end in self.s_rowspan.by_index[col_i].ranges:
|
||||||
|
for row_i in range(start + 1, end + 1):
|
||||||
|
self.rowspan_covered.add((row_i, col_i))
|
||||||
|
self.covered = self.colspan_covered | self.rowspan_covered
|
||||||
|
|
||||||
def remove_redundant_borders(self):
|
def remove_redundant_borders(self):
|
||||||
remove_right = []
|
remove_right = []
|
||||||
for row_i in range(self.row_count):
|
for row_i in range(self.row_count):
|
||||||
@@ -83,66 +203,76 @@ class Table(klammer_base.Klammer_base):
|
|||||||
a.border.right_all = False
|
a.border.right_all = False
|
||||||
|
|
||||||
def column_width_text(self):
|
def column_width_text(self):
|
||||||
|
# For each column, the text used to measure a 'fit' width in the
|
||||||
|
# tex target. The longest cell's font is applied, so a bold or
|
||||||
|
# italic cell is measured in the font it will be set in.
|
||||||
self.column_widths = []
|
self.column_widths = []
|
||||||
# print("column_width_text:", len(self.cells), len(self.cells[0]))
|
|
||||||
for col_i in range(len(self.cells[0])):
|
for col_i in range(len(self.cells[0])):
|
||||||
longest = ""
|
longest = ""
|
||||||
|
longest_font = "r"
|
||||||
for row_i in range(len(self.cells)):
|
for row_i in range(len(self.cells)):
|
||||||
cell = self.cells[row_i][col_i]
|
cell = self.cells[row_i][col_i]
|
||||||
if cell is not None:
|
# A colspan anchor's text spans several columns and must
|
||||||
cell_text = cell.text
|
# not set the width of its own column.
|
||||||
#lines = [e.strip() for e in cell_text.split("\\newline")]
|
if cell is not None and cell.colspan <= 1:
|
||||||
lines = [e.strip() for e in cell_text.split("\newline")]
|
lines = [e.strip() for e in cell.text.split("\\newline")]
|
||||||
lines = sorted(lines, key=len)
|
lines = sorted(lines, key=len)
|
||||||
longest_in_line = lines[-1]
|
longest_in_line = lines[-1]
|
||||||
if len(longest_in_line) > len(longest):
|
if len(longest_in_line) > len(longest):
|
||||||
longest = longest_in_line
|
longest = longest_in_line
|
||||||
|
longest_font = cell.font
|
||||||
|
if longest_font != "r":
|
||||||
|
longest = font.tex_fontify(longest, longest_font, 1.0)
|
||||||
self.column_widths.append(longest)
|
self.column_widths.append(longest)
|
||||||
# kutil.msg("column_widths:")
|
|
||||||
# print(self.column_widths)
|
|
||||||
|
|
||||||
def make_cells(self, rows):
|
def make_cells(self, rows):
|
||||||
|
self.compute_coverage()
|
||||||
result = []
|
result = []
|
||||||
cells = []
|
cells = []
|
||||||
for row_i, row in enumerate(rows):
|
for row_i, row in enumerate(rows):
|
||||||
row_cells = []
|
row_cells = []
|
||||||
for cell_i, cell in enumerate(row):
|
for cell_i, cell in enumerate(row):
|
||||||
rspan = self.span_count(self.s_rowspan, row_i, cell_i)
|
rspan = self.span_count(self.s_rowspan, cell_i, row_i)
|
||||||
cspan = self.span_count(self.s_colspan, row_i, cell_i)
|
cspan = self.span_count(self.s_colspan, row_i, cell_i)
|
||||||
font = self.font[cell_i]
|
font = self.font[cell_i]
|
||||||
if row_i == 0 and self.header:
|
if row_i == 0 and self.header:
|
||||||
font = self.header_font
|
font = self.header_font
|
||||||
|
# A span anchor's right and bottom borders come from the
|
||||||
|
# boundary at the END of the merged region.
|
||||||
|
right_i = cell_i + max(cspan, 1)
|
||||||
|
bottom_i = row_i + max(rspan, 1)
|
||||||
row_cells.append(
|
row_cells.append(
|
||||||
table_cell.Cell(
|
table_cell.Cell(
|
||||||
cell,
|
cell,
|
||||||
font, self.cell_hpos[cell_i],
|
font, self.cell_hpos[cell_i],
|
||||||
self.s_hline.has(row_i, cell_i),
|
self.s_hline.has(row_i, cell_i),
|
||||||
self.s_vline.has(cell_i+1, row_i),
|
self.s_vline.has(right_i, row_i),
|
||||||
self.s_hline.has(row_i+1, cell_i),
|
self.s_hline.has(bottom_i, cell_i),
|
||||||
self.s_vline.has(cell_i, row_i),
|
self.s_vline.has(cell_i, row_i),
|
||||||
self.s_vline.sequences.get(cell_i),
|
self.s_vline.by_index.get(cell_i),
|
||||||
self.s_vline.sequences.get(cell_i+1),
|
self.s_vline.by_index.get(right_i),
|
||||||
rspan, cspan))
|
rspan, cspan,
|
||||||
|
first_column=(cell_i == 0)))
|
||||||
cells.append(row_cells)
|
cells.append(row_cells)
|
||||||
widths = [len(e) for e in cells]
|
self.cells = cells
|
||||||
max_width = max(widths)
|
|
||||||
self.cells = [extend(row, max_width, None) for row in cells] \
|
|
||||||
if max_width != min(widths) else cells
|
|
||||||
self.column_width_text()
|
self.column_width_text()
|
||||||
|
|
||||||
# HTML
|
# HTML
|
||||||
|
|
||||||
def html(self):
|
def html(self):
|
||||||
result = ""
|
result = ""
|
||||||
for row in self.cells:
|
for row_i, row in enumerate(self.cells):
|
||||||
row_html = ""
|
row_html = ""
|
||||||
for cell in row:
|
for cell_i, cell in enumerate(row):
|
||||||
|
if (row_i, cell_i) in self.covered:
|
||||||
|
continue
|
||||||
row_html += cell.html().strip() + "\n"
|
row_html += cell.html().strip() + "\n"
|
||||||
result += E("tr").body(row_html).str()
|
result += E("tr").body(row_html).str()
|
||||||
result = E("table").body(result)
|
result = E("table").body(result)
|
||||||
if self.number or self.caption:
|
if self.number or self.caption:
|
||||||
result = html_util.add_caption(
|
result = html_util.add_caption(
|
||||||
result, "Table", self.number, self.caption, self.caption_font)
|
result, "Table", self.number, self.caption, self.caption_font,
|
||||||
|
side=self.caption_side, font_size=self.caption_font_size)
|
||||||
else:
|
else:
|
||||||
result = result.str()
|
result = result.str()
|
||||||
return result
|
return result
|
||||||
@@ -150,6 +280,9 @@ class Table(klammer_base.Klammer_base):
|
|||||||
# LaTeX
|
# LaTeX
|
||||||
|
|
||||||
def tex_hpos(self):
|
def tex_hpos(self):
|
||||||
|
# One column specification per column: the width comes from
|
||||||
|
# :column_width ('fit', a fraction of \tablewidth, or '*' for the
|
||||||
|
# remaining width), the justification from :cell_hpos.
|
||||||
def par_format(s, justification):
|
def par_format(s, justification):
|
||||||
command = {"l" : "raggedright",
|
command = {"l" : "raggedright",
|
||||||
"c" : "centering",
|
"c" : "centering",
|
||||||
@@ -157,68 +290,26 @@ class Table(klammer_base.Klammer_base):
|
|||||||
return f">{{\\{command}}}p{{{s}}}"
|
return f">{{\\{command}}}p{{{s}}}"
|
||||||
|
|
||||||
widths = []
|
widths = []
|
||||||
hpos_pat = re.compile("(f|(?:0?(\\.\\d+)(t))|\\*)?([lcr]?)")
|
for i, w in enumerate(extend(self.column_width, self.row_size)):
|
||||||
|
if w in ("fit", "f"):
|
||||||
def parse(s, width_text):
|
widths.append(f"\\widthof{{{self.column_widths[i]}}}")
|
||||||
match = hpos_pat.match(s)
|
elif w == "*":
|
||||||
# print("MATCH:", match, match.groups())
|
widths.append(None)
|
||||||
width, frac, table, just = match.groups()
|
|
||||||
width = width or "f"
|
|
||||||
just = just or "l"
|
|
||||||
if width and width[0] == "{":
|
|
||||||
width = f"\\widthof{{{s}}}"
|
|
||||||
elif table == "t":
|
|
||||||
width = f"{frac}\\tablewidth"
|
|
||||||
elif width == "f":
|
|
||||||
width = f"\\widthof{{{width_text}}}"
|
|
||||||
|
|
||||||
if width != "*":
|
|
||||||
widths.append(width)
|
|
||||||
|
|
||||||
result = par_format(width, just) if width != "*" else s
|
|
||||||
# print("PARSE:", result)
|
|
||||||
return result
|
|
||||||
|
|
||||||
# print("self.column_widths:", len(self.column_widths), self.column_widths)
|
|
||||||
# return extend([parse(e) for e in self.cell_hpos], self.row_size)
|
|
||||||
hpos_list = []
|
|
||||||
for i, hpos in enumerate(extend(self.cell_hpos, self.row_size)):
|
|
||||||
# print(f" Loop {i}:", hpos)
|
|
||||||
if i >= len(self.column_widths):
|
|
||||||
print(f"Warning: Ignoring table column width: {hpos}")
|
|
||||||
else:
|
else:
|
||||||
hpos_list.append(parse(hpos, self.column_widths[i]))
|
widths.append(f"{w}\\tablewidth")
|
||||||
|
fill_count = widths.count(None)
|
||||||
# print("hpos_list:", hpos_list)
|
|
||||||
fill_count = sum([1 if "*" in e else 0 for e in hpos_list])
|
|
||||||
# print("fill_count:", fill_count)
|
|
||||||
|
|
||||||
if fill_count > 0:
|
if fill_count > 0:
|
||||||
margins = f"(\\tabcolsep * {2 * len(hpos_list)})"
|
fixed = [e for e in widths if e is not None]
|
||||||
# print("MARGINS:", margins)
|
if fixed:
|
||||||
#expr = "\\linewidth - " + " - ".join(widths) + str("
|
expr = f"(\\tablewidth - {' - '.join(fixed)}) / {fill_count}"
|
||||||
if fill_count == len(hpos_list):
|
else:
|
||||||
expr = f"{1 / fill_count}\\tablewidth"
|
expr = f"{1 / fill_count}\\tablewidth"
|
||||||
else:
|
widths = [e if e is not None else expr for e in widths]
|
||||||
#expr = f"(\\textwidth - {margins} - {' - '.join(widths)}) / {fill_count}"
|
return [par_format(w, j) for w, j in zip(widths, self.cell_hpos)]
|
||||||
expr = f"(\\tablewidth - {' - '.join(widths)}) / {fill_count}"
|
|
||||||
# print(expr)
|
|
||||||
result = []
|
|
||||||
for h in hpos_list:
|
|
||||||
if h[0] == "*":
|
|
||||||
just = h[1] if len(h) > 1 else "l"
|
|
||||||
result.append(par_format(expr, just))
|
|
||||||
else:
|
|
||||||
result.append(h)
|
|
||||||
else:
|
|
||||||
result = hpos_list
|
|
||||||
# print("tex_hpos:", result)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def tex_column_spec(self):
|
def tex_column_spec(self):
|
||||||
parts = [""] * (self.row_size * 2 + 1)
|
parts = [""] * (self.row_size * 2 + 1)
|
||||||
for i in self.s_vline.sequences:
|
for i in self.s_vline.by_index:
|
||||||
parts[i * 2] = "|"
|
parts[i * 2] = "|"
|
||||||
for i, hpos in enumerate(self.tex_hpos()):
|
for i, hpos in enumerate(self.tex_hpos()):
|
||||||
parts[i * 2 + 1] = hpos
|
parts[i * 2 + 1] = hpos
|
||||||
@@ -226,30 +317,42 @@ class Table(klammer_base.Klammer_base):
|
|||||||
return "".join(parts)
|
return "".join(parts)
|
||||||
|
|
||||||
def tex_hline(self, index):
|
def tex_hline(self, index):
|
||||||
hline = ""
|
# Contiguous cell borders coalesce into single \cline runs; a
|
||||||
|
# full-width line becomes \hline.
|
||||||
bottom = index == self.row_count
|
bottom = index == self.row_count
|
||||||
if bottom:
|
if bottom:
|
||||||
index -= 1
|
index -= 1
|
||||||
count = 0
|
flags = [(cell.border.bottom if bottom else cell.border.top)
|
||||||
for i, cell in enumerate(self.cells[index]):
|
for cell in self.cells[index]]
|
||||||
has_border = cell.border.bottom if bottom else cell.border.top
|
if not bottom:
|
||||||
if has_border:
|
# No line through the interior of a merged (rowspan) cell.
|
||||||
hline += f"\\cline{{{i+1}-{i+1}}} "
|
flags = [flag and (index, col_i) not in self.rowspan_covered
|
||||||
count += 1
|
for col_i, flag in enumerate(flags)]
|
||||||
#if count == self.row_size:
|
if flags and all(flags):
|
||||||
# hline = "\\hline"
|
return "\\hline\n"
|
||||||
|
hline = ""
|
||||||
|
start = None
|
||||||
|
for i, flag in enumerate(flags + [False]):
|
||||||
|
if flag and start is None:
|
||||||
|
start = i
|
||||||
|
elif not flag and start is not None:
|
||||||
|
hline += f"\\cline{{{start + 1}-{i}}} "
|
||||||
|
start = None
|
||||||
return hline.strip() + "\n"
|
return hline.strip() + "\n"
|
||||||
|
|
||||||
def tex_rows(self):
|
def tex_rows(self):
|
||||||
result = ""
|
result = ""
|
||||||
for row_i, row in enumerate(self.cells):
|
for row_i, row in enumerate(self.cells):
|
||||||
result += self.tex_hline(row_i)
|
result += self.tex_hline(row_i)
|
||||||
tab = ""
|
parts = []
|
||||||
for cell_i, cell in enumerate(row):
|
for cell_i, cell in enumerate(row):
|
||||||
result += tab + cell.tex()
|
if (row_i, cell_i) in self.colspan_covered:
|
||||||
tab = " & "
|
continue # Absorbed by the \multicolumn anchor
|
||||||
#result += " \\\\\n"
|
if (row_i, cell_i) in self.rowspan_covered:
|
||||||
result += " \\tabularnewline\n"
|
parts.append("") # Occupied by the \multirow anchor
|
||||||
|
else:
|
||||||
|
parts.append(cell.tex())
|
||||||
|
result += " & ".join(parts) + " \\tabularnewline\n"
|
||||||
result += self.tex_hline(self.row_count)
|
result += self.tex_hline(self.row_count)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -277,9 +380,7 @@ class Table(klammer_base.Klammer_base):
|
|||||||
|
|
||||||
def tex(self):
|
def tex(self):
|
||||||
result = self.get_width()
|
result = self.get_width()
|
||||||
result += "\\vspace*{-.75\\baselineskip}"
|
result += f"\\renewcommand*{{\\arraystretch}}{{{self.leading}}}\n"
|
||||||
# result = ""
|
|
||||||
result += "\\renewcommand*{\\arraystretch}{1.3}\n"
|
|
||||||
if self.allow_break:
|
if self.allow_break:
|
||||||
result += "\\vspace*{12pt}\n"
|
result += "\\vspace*{12pt}\n"
|
||||||
result += "\\begin{longtable}{"
|
result += "\\begin{longtable}{"
|
||||||
@@ -292,17 +393,26 @@ class Table(klammer_base.Klammer_base):
|
|||||||
|
|
||||||
if not self.allow_break:
|
if not self.allow_break:
|
||||||
if self.number or self.caption:
|
if self.number or self.caption:
|
||||||
result = latex_util.add_caption(result, "Table", self.number, self.caption, "\\tablewidth")
|
result = latex_util.add_caption(
|
||||||
|
result, "Table", self.number, self.caption, "\\tablewidth",
|
||||||
|
side=self.caption_side, font_symbol=self.caption_font,
|
||||||
|
font_size=self.caption_font_size)
|
||||||
else:
|
else:
|
||||||
result = latex_util.caption_wrapper(result, "center")
|
result = latex_util.caption_wrapper(result, "center")
|
||||||
|
|
||||||
name = f"Reference-Table-{Table.id}"
|
name = f"Reference-Table-{Table.id}"
|
||||||
|
Table.id += 1
|
||||||
result = f"\\hypertarget{{{name}}}{{}}\\label{{Label-{name}}}\n{result}"
|
result = f"\\hypertarget{{{name}}}{{}}\\label{{Label-{name}}}\n{result}"
|
||||||
result = f"\\setlength{{\\tablewidth}}{{\\textwidth - {2 * self.row_count}\\tabcolsep}}\n" + result
|
# The wrapper (add_caption/caption_wrapper) owns all vertical space
|
||||||
# result += "\\vspace*{-8pt}"
|
# around the table; longtable's own glue is zeroed.
|
||||||
|
result = (f"\\setlength{{\\tabcolsep}}{{{self.colsep}}}\n"
|
||||||
|
"\\setlength{\\LTpre}{0pt}\n"
|
||||||
|
"\\setlength{\\LTpost}{0pt}\n"
|
||||||
|
f"\\setlength{{\\tablewidth}}{{\\textwidth - {2 * self.row_size}\\tabcolsep}}\n"
|
||||||
|
+ result)
|
||||||
result = re.sub(r"\newline", r"\\\\", result)
|
result = re.sub(r"\newline", r"\\\\", result)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def txt(self):
|
def txt(self):
|
||||||
return "TXT"
|
return "Table in .txt format not implemented"
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,25 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
import border
|
import border
|
||||||
import font
|
import font
|
||||||
import html_util
|
import html_util
|
||||||
from html_util import E
|
from html_util import E
|
||||||
|
|
||||||
|
# A purely numeric cell value (either decimal-mark style, optional sign).
|
||||||
|
number_rgx = re.compile(r"[-+]?[\d.,]+$")
|
||||||
|
|
||||||
class Cell:
|
class Cell:
|
||||||
def __init__(self, text, font, hpos,
|
def __init__(self, text, font, hpos,
|
||||||
top, right, bottom, left,
|
top, right, bottom, left,
|
||||||
left_all, right_all,
|
left_all, right_all,
|
||||||
rowspan, colspan):
|
rowspan, colspan, first_column=False):
|
||||||
self.text = text
|
self.text = text
|
||||||
self.font = font
|
self.font = font
|
||||||
self.hpos = hpos
|
self.hpos = hpos
|
||||||
self.border = border.Border(top, right, bottom, left, left_all, right_all)
|
self.border = border.Border(top, right, bottom, left, left_all, right_all)
|
||||||
self.rowspan = rowspan
|
self.rowspan = rowspan
|
||||||
self.colspan = colspan
|
self.colspan = colspan
|
||||||
|
self.first_column = first_column
|
||||||
#print("Cell:", text, hpos)
|
#print("Cell:", text, hpos)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@@ -30,6 +36,10 @@ class Cell:
|
|||||||
def html(self):
|
def html(self):
|
||||||
result = self.text
|
result = self.text
|
||||||
result = E("td").body(font.html_fontify(result, self.font, 1.0))
|
result = E("td").body(font.html_fontify(result, self.font, 1.0))
|
||||||
|
if self.rowspan > 1:
|
||||||
|
result.attr("rowspan", self.rowspan)
|
||||||
|
if self.colspan > 1:
|
||||||
|
result.attr("colspan", self.colspan)
|
||||||
for pred, cls_name in zip(self.border.has(), "Bt Br Bb Bl".split()):
|
for pred, cls_name in zip(self.border.has(), "Bt Br Bb Bl".split()):
|
||||||
if pred:
|
if pred:
|
||||||
result.cls(cls_name)
|
result.cls(cls_name)
|
||||||
@@ -38,8 +48,26 @@ class Cell:
|
|||||||
|
|
||||||
def tex(self, debug=False): # , left_line, right_line):
|
def tex(self, debug=False): # , left_line, right_line):
|
||||||
result = self.text
|
result = self.text
|
||||||
|
# A number must not line-break (LaTeX breaks after a minus sign
|
||||||
|
# read as a hyphen in narrow fit-width columns).
|
||||||
|
if number_rgx.match(result.strip()):
|
||||||
|
result = f"\\mbox{{{result.strip()}}}"
|
||||||
if self.font != "r":
|
if self.font != "r":
|
||||||
result = font.tex_fontify(result, self.font, 1.0)
|
result = font.tex_fontify(result, self.font, 1.0)
|
||||||
|
if self.rowspan > 1:
|
||||||
|
result = f"\\multirow{{{self.rowspan}}}{{*}}{{{result}}}"
|
||||||
|
if self.colspan > 1:
|
||||||
|
# \multicolumn carries the merged cell's own column spec. A
|
||||||
|
# left bar may only be given when the span starts at the
|
||||||
|
# table's first column: elsewhere the bar to the left belongs
|
||||||
|
# to the preceding column's preamble entry, and adding one
|
||||||
|
# here draws a doubled line.
|
||||||
|
pos = self.hpos
|
||||||
|
if self.border.left and self.first_column:
|
||||||
|
pos = "|" + pos
|
||||||
|
if self.border.right:
|
||||||
|
pos = pos + "|"
|
||||||
|
return f"\\multicolumn{{{self.colspan}}}{{{pos}}}{{{result}}}"
|
||||||
remove_left = self.border.left_all and not self.border.left
|
remove_left = self.border.left_all and not self.border.left
|
||||||
remove_right = self.border.right_all and not self.border.right
|
remove_right = self.border.right_all and not self.border.right
|
||||||
if debug:
|
if debug:
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ K := $(KLAMMERTEXT_HOME)
|
|||||||
KS := $(K)/sks
|
KS := $(K)/sks
|
||||||
KM := $(K)/mac
|
KM := $(K)/mac
|
||||||
|
|
||||||
include $(KM)/env/makefile.env
|
include $(K)/env/makefile.env
|
||||||
|
|
||||||
# Source files
|
# Source files
|
||||||
SOURCES := html_util.cpp latex_util.cpp font_resolve.cpp
|
SOURCES := html_util.cpp latex_util.cpp
|
||||||
OBJECTS := html_util.o latex_util.o font_resolve.o
|
OBJECTS := html_util.o latex_util.o
|
||||||
DEPFILES := html_util.d latex_util.d font_resolve.d
|
DEPFILES := html_util.d latex_util.d
|
||||||
|
|
||||||
# Additional include paths
|
# Additional include paths
|
||||||
LOCAL_CPPFLAGS := -I$(KM) -I$(KS)/kutil
|
LOCAL_CPPFLAGS := -I$(KM) -I$(KS)/kutil
|
||||||
|
|||||||
@@ -1,329 +0,0 @@
|
|||||||
#include "font_resolve.h"
|
|
||||||
#include "file.h"
|
|
||||||
#include "util.h"
|
|
||||||
#include "error.h"
|
|
||||||
#include "log.h"
|
|
||||||
#include "show.h"
|
|
||||||
#include "kutil.h"
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <regex>
|
|
||||||
#include <sstream>
|
|
||||||
#include <filesystem>
|
|
||||||
|
|
||||||
namespace fs = std::filesystem;
|
|
||||||
|
|
||||||
|
|
||||||
std::string name_to_dirname(std::string name)
|
|
||||||
{
|
|
||||||
std::string result {};
|
|
||||||
for (char c : name) {
|
|
||||||
if (c == ' ')
|
|
||||||
result += '-';
|
|
||||||
else
|
|
||||||
result += std::tolower(c);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
std::string name_to_google_query(std::string name)
|
|
||||||
{
|
|
||||||
std::string result {};
|
|
||||||
for (char c : name) {
|
|
||||||
if (c == ' ')
|
|
||||||
result += '+';
|
|
||||||
else
|
|
||||||
result += c;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static void classify_from_css(Resolved_font& font, std::string css_path)
|
|
||||||
{
|
|
||||||
// Parse the @font-face blocks in the CSS to determine variant → filename mapping
|
|
||||||
std::string css = string_from_file(css_path);
|
|
||||||
std::regex face_rgx(
|
|
||||||
R"(@font-face\s*\{[^}]*font-style:\s*(\w+);[^}]*font-weight:\s*(\w+);[^}]*url\('([^']+\.ttf)'\)[^}]*\})",
|
|
||||||
std::regex::multiline);
|
|
||||||
auto begin = std::sregex_iterator(css.begin(), css.end(), face_rgx);
|
|
||||||
auto end = std::sregex_iterator();
|
|
||||||
for (auto it = begin; it != end; ++it) {
|
|
||||||
std::string style = (*it)[1];
|
|
||||||
std::string weight = (*it)[2];
|
|
||||||
std::string url_path = (*it)[3];
|
|
||||||
// URL is like 'dir-name/Filename.ttf' — extract just the filename
|
|
||||||
std::string filename = url_path.substr(url_path.rfind('/') + 1);
|
|
||||||
bool is_bold = (weight == "700" || weight == "bold");
|
|
||||||
bool is_italic = (style == "italic" || style == "oblique");
|
|
||||||
if (is_bold && is_italic)
|
|
||||||
font.bold_italic = filename;
|
|
||||||
else if (is_bold)
|
|
||||||
font.bold = filename;
|
|
||||||
else if (is_italic)
|
|
||||||
font.italic = filename;
|
|
||||||
else
|
|
||||||
font.regular = filename;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Resolved_font resolve_bundled(std::string family_name, std::string dir_name)
|
|
||||||
{
|
|
||||||
std::string bundled_dir = klammertext_dir() + "/sks/font/fonts/" + dir_name;
|
|
||||||
std::string bundled_css = bundled_dir + ".css";
|
|
||||||
if (fs::exists(bundled_dir) && fs::exists(bundled_css)) {
|
|
||||||
Resolved_font font {};
|
|
||||||
font.family_name = family_name;
|
|
||||||
font.dir_name = dir_name;
|
|
||||||
font.font_dir = bundled_dir;
|
|
||||||
font.css_file = bundled_css;
|
|
||||||
font.from_cache = false;
|
|
||||||
classify_from_css(font, bundled_css);
|
|
||||||
return font;
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static Resolved_font resolve_cached(std::string family_name, std::string dir_name)
|
|
||||||
{
|
|
||||||
std::string cache_base = cache_directory("_fonts");
|
|
||||||
std::string cache_dir = cache_base + "/" + dir_name;
|
|
||||||
std::string cache_css = cache_base + "/" + dir_name + ".css";
|
|
||||||
if (fs::exists(cache_dir) && fs::exists(cache_css)) {
|
|
||||||
Resolved_font font {};
|
|
||||||
font.family_name = family_name;
|
|
||||||
font.dir_name = dir_name;
|
|
||||||
font.font_dir = cache_dir;
|
|
||||||
font.css_file = cache_css;
|
|
||||||
font.from_cache = true;
|
|
||||||
classify_from_css(font, cache_css);
|
|
||||||
return font;
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static Resolved_font fetch_google_font(std::string family_name, std::string dir_name)
|
|
||||||
{
|
|
||||||
(void)K::log(1, "Fetching font \"" + family_name + "\" from Google Fonts");
|
|
||||||
|
|
||||||
std::string query = name_to_google_query(family_name);
|
|
||||||
std::string url =
|
|
||||||
"https://fonts.googleapis.com/css2?family=" + query +
|
|
||||||
":ital,wght@0,400;0,700;1,400;1,700&display=swap";
|
|
||||||
|
|
||||||
std::string cmd = "curl -s -H 'User-Agent: Mozilla/4.0' '" + url + "'";
|
|
||||||
std::string css_response = exec(cmd.c_str());
|
|
||||||
|
|
||||||
if (css_response.empty() || css_response.find("@font-face") == std::string::npos) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse @font-face blocks to extract style, weight, and .ttf URL
|
|
||||||
struct Font_variant {
|
|
||||||
std::string style; // "normal" or "italic"
|
|
||||||
std::string weight; // "400" or "700"
|
|
||||||
std::string url;
|
|
||||||
};
|
|
||||||
std::vector<Font_variant> variants {};
|
|
||||||
|
|
||||||
std::regex face_rgx(
|
|
||||||
R"(@font-face\s*\{[^}]*font-style:\s*(\w+);[^}]*font-weight:\s*(\d+);[^}]*src:\s*url\((https?://[^)]+\.ttf)\)[^}]*\})",
|
|
||||||
std::regex::multiline);
|
|
||||||
|
|
||||||
auto begin = std::sregex_iterator(css_response.begin(), css_response.end(), face_rgx);
|
|
||||||
auto end = std::sregex_iterator();
|
|
||||||
|
|
||||||
for (auto it = begin; it != end; ++it) {
|
|
||||||
Font_variant v {};
|
|
||||||
v.style = (*it)[1];
|
|
||||||
v.weight = (*it)[2];
|
|
||||||
v.url = (*it)[3];
|
|
||||||
variants.push_back(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (variants.empty()) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create cache directory
|
|
||||||
std::string cache_base = cache_directory("_fonts");
|
|
||||||
if (!fs::exists(cache_base))
|
|
||||||
fs::create_directories(cache_base);
|
|
||||||
|
|
||||||
std::string cache_dir = cache_base + "/" + dir_name;
|
|
||||||
if (!fs::exists(cache_dir))
|
|
||||||
fs::create_directory(cache_dir);
|
|
||||||
|
|
||||||
Resolved_font font {};
|
|
||||||
font.family_name = family_name;
|
|
||||||
font.dir_name = dir_name;
|
|
||||||
font.font_dir = cache_dir;
|
|
||||||
font.from_cache = true;
|
|
||||||
|
|
||||||
// Download each .ttf file with descriptive names
|
|
||||||
for (auto& v : variants) {
|
|
||||||
std::string local_name;
|
|
||||||
if (v.style == "normal" && v.weight == "400")
|
|
||||||
local_name = "Regular.ttf";
|
|
||||||
else if (v.style == "normal" && v.weight == "700")
|
|
||||||
local_name = "Bold.ttf";
|
|
||||||
else if (v.style == "italic" && v.weight == "400")
|
|
||||||
local_name = "Italic.ttf";
|
|
||||||
else if (v.style == "italic" && v.weight == "700")
|
|
||||||
local_name = "BoldItalic.ttf";
|
|
||||||
else
|
|
||||||
continue;
|
|
||||||
|
|
||||||
std::string ttf_path = cache_dir + "/" + local_name;
|
|
||||||
|
|
||||||
if (!fs::exists(ttf_path)) {
|
|
||||||
std::string dl_cmd = "curl -s -o '" + ttf_path + "' '" + v.url + "'";
|
|
||||||
(void)exec(dl_cmd.c_str());
|
|
||||||
if (!fs::exists(ttf_path)) {
|
|
||||||
(void)K::log(1, "Failed to download font file: " + v.url);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (local_name == "Regular.ttf")
|
|
||||||
font.regular = local_name;
|
|
||||||
else if (local_name == "Bold.ttf")
|
|
||||||
font.bold = local_name;
|
|
||||||
else if (local_name == "Italic.ttf")
|
|
||||||
font.italic = local_name;
|
|
||||||
else if (local_name == "BoldItalic.ttf")
|
|
||||||
font.bold_italic = local_name;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate @font-face CSS file
|
|
||||||
std::string css_file = cache_base + "/" + dir_name + ".css";
|
|
||||||
std::stringstream css {};
|
|
||||||
auto emit_face = [&](std::string style, std::string weight, std::string filename) {
|
|
||||||
if (filename.empty())
|
|
||||||
return;
|
|
||||||
css << "\n@font-face {\n"
|
|
||||||
<< " font-family: '" << family_name << "';\n"
|
|
||||||
<< " font-style: " << style << ";\n"
|
|
||||||
<< " font-weight: " << weight << ";\n"
|
|
||||||
<< " src: url('" << dir_name << "/" << filename << "') format('truetype');\n"
|
|
||||||
<< "}\n";
|
|
||||||
};
|
|
||||||
emit_face("normal", "400", font.regular);
|
|
||||||
emit_face("normal", "700", font.bold);
|
|
||||||
emit_face("italic", "400", font.italic);
|
|
||||||
emit_face("italic", "700", font.bold_italic);
|
|
||||||
|
|
||||||
string_to_file(css_file, css.str());
|
|
||||||
font.css_file = css_file;
|
|
||||||
|
|
||||||
return font;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static void extract_font_metrics(Resolved_font& font)
|
|
||||||
{
|
|
||||||
if (font.regular.empty() || font.font_dir.empty())
|
|
||||||
return;
|
|
||||||
std::string ttf_path = font.font_dir + "/" + font.regular;
|
|
||||||
if (!fs::exists(ttf_path))
|
|
||||||
return;
|
|
||||||
// Extract both x-height and cap-height ratios from OS/2 table
|
|
||||||
std::string script =
|
|
||||||
"python3 -c \""
|
|
||||||
"import struct; "
|
|
||||||
"f = open('" + ttf_path + "', 'rb'); "
|
|
||||||
"_, n = struct.unpack('>IH', f.read(6)); "
|
|
||||||
"f.read(6); "
|
|
||||||
"t = {};\n"
|
|
||||||
"for _ in range(n):\n"
|
|
||||||
" tag = f.read(4).decode('latin-1').strip('\\\\x00'); "
|
|
||||||
" _, o, l = struct.unpack('>III', f.read(12)); "
|
|
||||||
" t[tag] = o\n"
|
|
||||||
"f.seek(t['head'] + 18); "
|
|
||||||
"upm = struct.unpack('>H', f.read(2))[0]; "
|
|
||||||
"f.seek(t['OS/2']); "
|
|
||||||
"ver = struct.unpack('>H', f.read(2))[0]; "
|
|
||||||
"f.seek(t['OS/2'] + 86); "
|
|
||||||
"xh, ch = struct.unpack('>hh', f.read(4)); "
|
|
||||||
"print(f'{xh/upm:.4f} {ch/upm:.4f}') if ver >= 2 else None; "
|
|
||||||
"f.close()\"";
|
|
||||||
std::string result = trim(exec(script.c_str()));
|
|
||||||
if (!result.empty()) {
|
|
||||||
try {
|
|
||||||
auto pos = result.find(' ');
|
|
||||||
if (pos != std::string::npos) {
|
|
||||||
font.xheight_ratio = std::stof(result.substr(0, pos));
|
|
||||||
font.capheight_ratio = std::stof(result.substr(pos + 1));
|
|
||||||
}
|
|
||||||
} catch (...) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Resolved_font resolve_font(std::string family_name)
|
|
||||||
{
|
|
||||||
if (family_name.empty())
|
|
||||||
return {};
|
|
||||||
|
|
||||||
std::string dir_name = name_to_dirname(family_name);
|
|
||||||
|
|
||||||
// 1. Check bundled fonts
|
|
||||||
Resolved_font font = resolve_bundled(family_name, dir_name);
|
|
||||||
if (!font.family_name.empty()) {
|
|
||||||
extract_font_metrics(font);
|
|
||||||
return font;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Check font cache
|
|
||||||
font = resolve_cached(family_name, dir_name);
|
|
||||||
if (!font.family_name.empty()) {
|
|
||||||
extract_font_metrics(font);
|
|
||||||
return font;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Fetch from Google Fonts
|
|
||||||
font = fetch_google_font(family_name, dir_name);
|
|
||||||
if (!font.family_name.empty()) {
|
|
||||||
extract_font_metrics(font);
|
|
||||||
return font;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Error
|
|
||||||
throw Argument_error(
|
|
||||||
"Font \"" + family_name + "\" not found.\n"
|
|
||||||
" Not bundled in sks/font/fonts/" + dir_name + "/,\n"
|
|
||||||
" not cached, and not available from Google Fonts.\n"
|
|
||||||
" Check the font name or install it locally.");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Font assets are copied with copy_file_stream() (mac/file.h) rather than
|
|
||||||
// std::filesystem::copy_file, which fails on Apple `container` virtiofs mounts
|
|
||||||
// — see the note on copy_file_stream() in file.cpp for the full rationale.
|
|
||||||
|
|
||||||
|
|
||||||
void install_resolved_font(const Resolved_font& font, std::string output_dir)
|
|
||||||
{
|
|
||||||
if (font.family_name.empty())
|
|
||||||
return;
|
|
||||||
|
|
||||||
std::string output_font_dir = output_dir + "/fonts";
|
|
||||||
if (!fs::exists(output_font_dir))
|
|
||||||
fs::create_directory(output_font_dir);
|
|
||||||
|
|
||||||
// Copy .css file and font directory to output
|
|
||||||
std::string dest_css = output_font_dir + "/" + font.dir_name + ".css";
|
|
||||||
std::string dest_dir = output_font_dir + "/" + font.dir_name;
|
|
||||||
|
|
||||||
copy_file_stream(font.css_file, dest_css);
|
|
||||||
|
|
||||||
if (!fs::exists(dest_dir)) {
|
|
||||||
fs::create_directory(dest_dir);
|
|
||||||
for (auto& entry : fs::directory_iterator(font.font_dir)) {
|
|
||||||
copy_file_stream(entry.path(),
|
|
||||||
dest_dir + "/" + entry.path().filename().string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
struct Resolved_font {
|
|
||||||
std::string family_name {}; // "Crimson Pro"
|
|
||||||
std::string dir_name {}; // "crimson-pro"
|
|
||||||
std::string font_dir {}; // Full path to font directory
|
|
||||||
std::string css_file {}; // Full path to .css file
|
|
||||||
bool from_cache = false;
|
|
||||||
// .ttf filenames for each variant (empty if variant not available):
|
|
||||||
std::string regular {};
|
|
||||||
std::string bold {};
|
|
||||||
std::string italic {};
|
|
||||||
std::string bold_italic {};
|
|
||||||
float xheight_ratio = 0.0f; // x-height / unitsPerEm from OS/2 table
|
|
||||||
float capheight_ratio = 0.0f; // cap-height / unitsPerEm from OS/2 table
|
|
||||||
};
|
|
||||||
|
|
||||||
std::string name_to_dirname(std::string name);
|
|
||||||
std::string name_to_google_query(std::string name);
|
|
||||||
Resolved_font resolve_font(std::string family_name);
|
|
||||||
void install_resolved_font(const Resolved_font& font, std::string output_dir);
|
|
||||||
@@ -254,25 +254,6 @@ namespace html {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
elements_t google_font_elements(strings_t fontnames)
|
|
||||||
{
|
|
||||||
elements_t result {};
|
|
||||||
if (fontnames.size() > 0) {
|
|
||||||
result.push_back(elt("link")
|
|
||||||
.attr("rel", "preconnect")
|
|
||||||
.attr("href", "https://fonts.googleapis.com"));
|
|
||||||
result.push_back(elt("link")
|
|
||||||
.attr("rel", "preconnect")
|
|
||||||
.attr("href", "https://fonts.gstatic.com"));
|
|
||||||
}
|
|
||||||
for (auto name : fontnames)
|
|
||||||
result.push_back(
|
|
||||||
elt("link")
|
|
||||||
.attr("href", "https://fonts.googleapis.com/css2?family=" + name + "&display=swap")
|
|
||||||
.attr("rel", "stylesheet"));
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
elements_t javascript(std::string output_dir, strings_t js_filenames)
|
elements_t javascript(std::string output_dir, strings_t js_filenames)
|
||||||
{
|
{
|
||||||
//elements_t result { jquery_elements() };
|
//elements_t result { jquery_elements() };
|
||||||
@@ -298,15 +279,12 @@ namespace html {
|
|||||||
HTML head(std::string title,
|
HTML head(std::string title,
|
||||||
std::string css,
|
std::string css,
|
||||||
strings_t css_filenames,
|
strings_t css_filenames,
|
||||||
strings_t local_fonts, strings_t google_fonts)
|
strings_t local_fonts)
|
||||||
{
|
{
|
||||||
elements_t elts = meta_elements();
|
elements_t elts = meta_elements();
|
||||||
// msg() << "ELTS sks: " << elts << "\n";
|
// msg() << "ELTS sks: " << elts << "\n";
|
||||||
for (auto e : local_font_elements(local_fonts))
|
for (auto e : local_font_elements(local_fonts))
|
||||||
elts.push_back(e);
|
elts.push_back(e);
|
||||||
for (auto e : google_font_elements(google_fonts))
|
|
||||||
elts.push_back(e);
|
|
||||||
//elts += google_font_prolog();
|
|
||||||
|
|
||||||
//std::string css_dir = output_dir + "/css/";
|
//std::string css_dir = output_dir + "/css/";
|
||||||
// No; a single file at the top level...?
|
// No; a single file at the top level...?
|
||||||
@@ -471,55 +449,6 @@ namespace html {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
void install_local_fonts(strings_t font_dirs, strings_t names, std::string output_dir)
|
|
||||||
{
|
|
||||||
std::string output_font_dir = output_dir + "/fonts";
|
|
||||||
//std::cout << "Font directory for website: " << output_font_dir << "\n";
|
|
||||||
if (!file_exists(output_font_dir)) {
|
|
||||||
fs::create_directory(output_font_dir);
|
|
||||||
//std::cout << " Font directory created: " << output_font_dir << "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto name : names) {
|
|
||||||
//std::cout << "Font search for " << name << ": " << klammertext_dir() << "/sks/font/fonts/\n";
|
|
||||||
std::string src_font_dir = klammertext_dir() + "/sks/font/fonts/" + name;
|
|
||||||
if (!file_exists(src_font_dir)) {
|
|
||||||
bool found = false;
|
|
||||||
for (std::string font_dir : font_dirs) {
|
|
||||||
src_font_dir = font_dir + "/" + name;
|
|
||||||
//std::cout << "Font search for " << name << ": " << font_dir << "/\n";
|
|
||||||
if (file_exists(src_font_dir)) {
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!found) {
|
|
||||||
throw Argument_error(
|
|
||||||
"Local font directory \"" + src_font_dir + "\" does not exist");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//std::cout << "Font " << name << ": " << src_font_dir << "\n";
|
|
||||||
|
|
||||||
std::string src_font_css = src_font_dir + ".css";
|
|
||||||
if (!fs::exists(src_font_css)) {
|
|
||||||
throw Argument_error(
|
|
||||||
"Local font CSS file \"" + src_font_css + "\" does not exist");
|
|
||||||
}
|
|
||||||
std::string font_copy =
|
|
||||||
"cp -r " + src_font_css + " " + src_font_dir + " " + output_font_dir;
|
|
||||||
|
|
||||||
//std::string files_copy = "cp -r " + basename + " " + font_dir;
|
|
||||||
//cout << css_copy << "\n" << files_copy << "\n";
|
|
||||||
//std::cout << system(css_copy.c_str()) << "\n";
|
|
||||||
//std::cout << system(files_copy.c_str()) << "\n";
|
|
||||||
|
|
||||||
if (system(font_copy.c_str()) != 0) {
|
|
||||||
std::cout << "Font copy command: " << font_copy << "\n";
|
|
||||||
throw Argument_error("Local font \"" + name + "\" not found");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
elements_t status(std::string date, std::string version, std::string copyright)
|
elements_t status(std::string date, std::string version, std::string copyright)
|
||||||
{
|
{
|
||||||
elements_t result {};
|
elements_t result {};
|
||||||
@@ -550,7 +479,6 @@ namespace html {
|
|||||||
strings_t css_filenames,
|
strings_t css_filenames,
|
||||||
strings_t js_filenames,
|
strings_t js_filenames,
|
||||||
strings_t local_fonts,
|
strings_t local_fonts,
|
||||||
strings_t google_fonts,
|
|
||||||
std::string logo)
|
std::string logo)
|
||||||
{
|
{
|
||||||
if (page_title.empty())
|
if (page_title.empty())
|
||||||
@@ -601,7 +529,7 @@ namespace html {
|
|||||||
page_title = title;
|
page_title = title;
|
||||||
result.push_back(
|
result.push_back(
|
||||||
elt("html",
|
elt("html",
|
||||||
{ head(page_title, css, css_filenames, local_fonts, google_fonts),
|
{ head(page_title, css, css_filenames, local_fonts),
|
||||||
elt("body", body)
|
elt("body", body)
|
||||||
}).attr("lang", "en"));
|
}).attr("lang", "en"));
|
||||||
return result;
|
return result;
|
||||||
@@ -666,12 +594,12 @@ namespace html {
|
|||||||
elements_t make_page(
|
elements_t make_page(
|
||||||
elements_t body,
|
elements_t body,
|
||||||
std::string page_title, std::string css,
|
std::string page_title, std::string css,
|
||||||
strings_t css_filenames, strings_t local_fonts, strings_t google_fonts)
|
strings_t css_filenames, strings_t local_fonts)
|
||||||
{
|
{
|
||||||
elements_t page { preamble() };
|
elements_t page { preamble() };
|
||||||
page.push_back(
|
page.push_back(
|
||||||
elt("html",
|
elt("html",
|
||||||
{ head(page_title, css, css_filenames, local_fonts, google_fonts),
|
{ head(page_title, css, css_filenames, local_fonts),
|
||||||
elt("body", body)
|
elt("body", body)
|
||||||
}).attr("lang", "en"));
|
}).attr("lang", "en"));
|
||||||
return page;
|
return page;
|
||||||
|
|||||||
@@ -76,8 +76,6 @@ namespace html {
|
|||||||
//HTML preamble();
|
//HTML preamble();
|
||||||
//HTML head(strings_t css_filenames, strings_t js_filenames);
|
//HTML head(strings_t css_filenames, strings_t js_filenames);
|
||||||
|
|
||||||
void install_local_fonts(strings_t font_dirs, strings_t names, std::string output_dir);
|
|
||||||
|
|
||||||
elements_t page(
|
elements_t page(
|
||||||
std::string title,
|
std::string title,
|
||||||
std::string page_title,
|
std::string page_title,
|
||||||
@@ -93,7 +91,6 @@ namespace html {
|
|||||||
strings_t css_filenames = {},
|
strings_t css_filenames = {},
|
||||||
strings_t js_filenames = {},
|
strings_t js_filenames = {},
|
||||||
strings_t local_fonts = {},
|
strings_t local_fonts = {},
|
||||||
strings_t google_fonts = {},
|
|
||||||
std::string logo = {});
|
std::string logo = {});
|
||||||
|
|
||||||
void add_title(elements_t& body, std::string title, std::string logo="");
|
void add_title(elements_t& body, std::string title, std::string logo="");
|
||||||
@@ -108,8 +105,7 @@ namespace html {
|
|||||||
elements_t body,
|
elements_t body,
|
||||||
std::string page_title, std::string css,
|
std::string page_title, std::string css,
|
||||||
std::vector<std::string> css_filenames,
|
std::vector<std::string> css_filenames,
|
||||||
std::vector<std::string> local_fonts,
|
std::vector<std::string> local_fonts);
|
||||||
std::vector<std::string> google_fonts);
|
|
||||||
|
|
||||||
bool tag_is_block_element(std::string tag);
|
bool tag_is_block_element(std::string tag);
|
||||||
std::string make_paragraphs(std::string html_text);
|
std::string make_paragraphs(std::string html_text);
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ def add_caption(element, caption_label, number, caption_text,
|
|||||||
tag = element_tag(element)
|
tag = element_tag(element)
|
||||||
# Caption
|
# Caption
|
||||||
caption = ""
|
caption = ""
|
||||||
if number == "true":
|
if number:
|
||||||
caption = kutil.caption_marker(caption_label, caption_text)
|
caption = kutil.caption_marker(caption_label, caption_text)
|
||||||
elif caption_text:
|
elif caption_text:
|
||||||
caption = caption_text
|
caption = caption_text
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
#include "util.h"
|
#include "util.h"
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
#include "show.h"
|
#include "show.h"
|
||||||
#include "font_resolve.h"
|
#include "font_store.h"
|
||||||
|
|
||||||
namespace latex {
|
namespace latex {
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include "font_resolve.h"
|
#include "font_store.h"
|
||||||
|
|
||||||
namespace latex {
|
namespace latex {
|
||||||
|
|
||||||
|
|||||||
@@ -73,9 +73,8 @@ def make_caption_text(number, label, text, font_symbol, font_size):
|
|||||||
# caption = text # f"{{\\small \\it \\par {caption_text}}}"
|
# caption = text # f"{{\\small \\it \\par {caption_text}}}"
|
||||||
caption = text
|
caption = text
|
||||||
|
|
||||||
font_symbol = "r"
|
if caption is None:
|
||||||
font_size = 1.2
|
return ""
|
||||||
|
|
||||||
caption = font.tex_fontify(caption, font_symbol, font_size)
|
caption = font.tex_fontify(caption, font_symbol, font_size)
|
||||||
return caption
|
return caption
|
||||||
|
|
||||||
@@ -108,9 +107,9 @@ def add_caption(element, caption_label, number, caption_text, latex_width,
|
|||||||
# minipage(caption),
|
# minipage(caption),
|
||||||
latex_width, vertical="t")
|
latex_width, vertical="t")
|
||||||
elif side == "top":
|
elif side == "top":
|
||||||
element = minipage(caption + "\n" + element,
|
# A depth strut: the bottom-caption case uses \vstrut (height)
|
||||||
# minipage(caption, vertical="t") +
|
# above the caption; a top caption needs the mirror image,
|
||||||
# caption_margin + "\n" +
|
# space below its line.
|
||||||
# minipage(element),
|
element = minipage(caption + "\\rule[-0.75\\baselineskip]{0pt}{0pt}\n" + element,
|
||||||
latex_width, vertical="t")
|
latex_width, vertical="t")
|
||||||
return caption_wrapper(element, hpos)
|
return caption_wrapper(element, hpos)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
# theoretical basis (operadic arity, the precedence-order proposition, and
|
# theoretical basis (operadic arity, the precedence-order proposition, and
|
||||||
# @cond as a non-strict special form).
|
# @cond as a non-strict special form).
|
||||||
#
|
#
|
||||||
# Usage: ./cond_test.sh (LSan suppressions come from mac/env/runtime.env.*)
|
# Usage: ./cond_test.sh (LSan suppressions come from env/runtime.env.*)
|
||||||
# Exit code: 0 if all tests pass, 1 otherwise.
|
# Exit code: 0 if all tests pass, 1 otherwise.
|
||||||
|
|
||||||
PASS=0
|
PASS=0
|
||||||
|
|||||||
Reference in New Issue
Block a user