Offer-motivated features: :hline defaults, ranged :hpos, @date :days, :bottom none
- @table: a writer's :hline/:vline replaces the default lines; new
boundary name 'none' removes all lines
- @table: ranged :hpos argument overrides :cell_hpos per cell
(\multicolumn{1} in tex; positions a colspan anchor's merged cell)
- @date/@datetime: :days offset argument (sks/date/date.py)
- @document: ":bottom none" suppresses the footer (\pagestyle{empty})
Also carries the escape-system generator/renderer fixes, per-cell-range
:format, and uppercase .TTF/.OTF font recognition from klammertext-dev.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,6 +2,7 @@
|
|||||||
*.o
|
*.o
|
||||||
*.d
|
*.d
|
||||||
*.so
|
*.so
|
||||||
|
__pycache__/
|
||||||
|
|
||||||
# Editor / OS cruft
|
# Editor / OS cruft
|
||||||
*~
|
*~
|
||||||
|
|||||||
83
mac/.clang-tidy
Normal file
83
mac/.clang-tidy
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
Checks: >
|
||||||
|
*,
|
||||||
|
-boost-*,
|
||||||
|
-fuchsia-*,
|
||||||
|
-google-*,
|
||||||
|
-zircon-*,
|
||||||
|
-abseil-*,
|
||||||
|
-modernize-use-trailing-return-type,
|
||||||
|
-llvmlibc-*,
|
||||||
|
-altera-*,
|
||||||
|
-android-*,
|
||||||
|
-objc-*,
|
||||||
|
-fuchsia-*,
|
||||||
|
-hicpp-*,
|
||||||
|
-misc-*,
|
||||||
|
-performance-*,
|
||||||
|
-portability-*,
|
||||||
|
-readability-*,
|
||||||
|
-bugprone-*,
|
||||||
|
-cert-*,
|
||||||
|
-clang-analyzer-*,
|
||||||
|
-cppcoreguidelines-*,
|
||||||
|
-google-*,
|
||||||
|
-hicpp-*,
|
||||||
|
-llvm-*,
|
||||||
|
-llvmlibc-*,
|
||||||
|
-misc-*,
|
||||||
|
-modernize-*,
|
||||||
|
-performance-*,
|
||||||
|
-portability-*,
|
||||||
|
-readability-*,
|
||||||
|
-zircon-*
|
||||||
|
|
||||||
|
WarningsAsErrors: ''
|
||||||
|
HeaderFilterRegex: '.*'
|
||||||
|
FormatStyle: none
|
||||||
|
CheckOptions:
|
||||||
|
- key: readability-identifier-naming.ClassCase
|
||||||
|
value: CamelCase
|
||||||
|
- key: readability-identifier-naming.ClassMemberCase
|
||||||
|
value: camelBack
|
||||||
|
- key: readability-identifier-naming.ConstexprVariableCase
|
||||||
|
value: CamelCase
|
||||||
|
- key: readability-identifier-naming.ConstexprVariablePrefix
|
||||||
|
value: k
|
||||||
|
- key: readability-identifier-naming.EnumCase
|
||||||
|
value: CamelCase
|
||||||
|
- key: readability-identifier-naming.EnumConstantCase
|
||||||
|
value: CamelCase
|
||||||
|
- key: readability-identifier-naming.FunctionCase
|
||||||
|
value: camelBack
|
||||||
|
- key: readability-identifier-naming.GlobalConstantCase
|
||||||
|
value: CamelCase
|
||||||
|
- key: readability-identifier-naming.GlobalConstantPrefix
|
||||||
|
value: k
|
||||||
|
- key: readability-identifier-naming.GlobalVariableCase
|
||||||
|
value: camelBack
|
||||||
|
- key: readability-identifier-naming.GlobalVariablePrefix
|
||||||
|
value: g_
|
||||||
|
- key: readability-identifier-naming.LocalConstantCase
|
||||||
|
value: camelBack
|
||||||
|
- key: readability-identifier-naming.LocalVariableCase
|
||||||
|
value: camelBack
|
||||||
|
- key: readability-identifier-naming.MemberCase
|
||||||
|
value: camelBack
|
||||||
|
- key: readability-identifier-naming.MemberPrefix
|
||||||
|
value: m_
|
||||||
|
- key: readability-identifier-naming.NamespaceCase
|
||||||
|
value: lower_case
|
||||||
|
- key: readability-identifier-naming.PrivateMemberPrefix
|
||||||
|
value: m_
|
||||||
|
- key: readability-identifier-naming.StaticConstantCase
|
||||||
|
value: CamelCase
|
||||||
|
- key: readability-identifier-naming.StaticConstantPrefix
|
||||||
|
value: k
|
||||||
|
- key: readability-identifier-naming.StaticVariableCase
|
||||||
|
value: camelBack
|
||||||
|
- key: readability-identifier-naming.StaticVariablePrefix
|
||||||
|
value: s_
|
||||||
|
- key: readability-identifier-naming.TemplateParameterCase
|
||||||
|
value: CamelCase
|
||||||
|
- key: readability-identifier-naming.VariableCase
|
||||||
|
value: camelBack
|
||||||
14
mac/eval.cpp
14
mac/eval.cpp
@@ -6,6 +6,7 @@
|
|||||||
#include "show.h"
|
#include "show.h"
|
||||||
#include "katom.h"
|
#include "katom.h"
|
||||||
#include "file.h"
|
#include "file.h"
|
||||||
|
#include <algorithm>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
std::string shell(State state, std::string command, Locator loc)
|
std::string shell(State state, std::string command, Locator loc)
|
||||||
@@ -151,8 +152,19 @@ katom_list Eval::eval(katom_iter begin, katom_iter end)
|
|||||||
}
|
}
|
||||||
katom_list result {};
|
katom_list result {};
|
||||||
Machine M = m_machine;
|
Machine M = m_machine;
|
||||||
|
size_t before = M.m_katoms.size();
|
||||||
M.read(eval_result);
|
M.read(eval_result);
|
||||||
M.apply(M.m_state.value("K_target"), false, false);
|
// If the @eval produced more Klammertext -- the read-back result still
|
||||||
|
// holds klammers to reduce (a "generator", e.g. a Python klammer returning
|
||||||
|
// a @table call with data) -- then its text is writer content: escape it
|
||||||
|
// for the target before applying, exactly as typed input is escaped, so a
|
||||||
|
// "%" in the data becomes "\%". If it produced final target markup (no
|
||||||
|
// klammers left, a "renderer" such as @table itself emitting \begin{tabular})
|
||||||
|
// leave it untouched. ^'...'^ literal spans are skipped by the escape pass,
|
||||||
|
// so a generator can still carry raw target markup.
|
||||||
|
bool has_klammer = std::any_of(
|
||||||
|
M.m_katoms.begin() + before, M.m_katoms.end(), begin_apply);
|
||||||
|
M.apply(M.m_state.value("K_target"), false, has_klammer);
|
||||||
result = trim(M.m_katoms);
|
result = trim(M.m_katoms);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,18 @@
|
|||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
|
||||||
|
// A filesystem extension lowercased for case-insensitive matching, so a
|
||||||
|
// font named Foo.TTF or Foo.OTF is recognized like Foo.ttf / Foo.otf.
|
||||||
|
static std::string lower_extension(const fs::path& path)
|
||||||
|
{
|
||||||
|
std::string ext = path.extension();
|
||||||
|
for (char& c : ext) {
|
||||||
|
c = std::tolower(static_cast<unsigned char>(c));
|
||||||
|
}
|
||||||
|
return ext;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
std::string name_to_dirname(std::string name)
|
std::string name_to_dirname(std::string name)
|
||||||
{
|
{
|
||||||
std::string result {};
|
std::string result {};
|
||||||
@@ -275,7 +287,10 @@ Font_file classify_font_file(const std::string& path)
|
|||||||
{
|
{
|
||||||
Font_file file {};
|
Font_file file {};
|
||||||
file.path = path;
|
file.path = path;
|
||||||
file.extension = fs::path(path).extension();
|
// Normalize to lowercase so the installed filename, its @font-face url,
|
||||||
|
// and the truetype/opentype format detection are uniform regardless of
|
||||||
|
// how the source file's extension was capitalized.
|
||||||
|
file.extension = lower_extension(path);
|
||||||
|
|
||||||
std::string d = read_binary_file(path);
|
std::string d = read_binary_file(path);
|
||||||
if (d.size() < 12) {
|
if (d.size() < 12) {
|
||||||
@@ -372,7 +387,7 @@ std::vector<Font_file> classify_font_files(const std::string& directory)
|
|||||||
"The font directory \"" + directory + "\" does not exist");
|
"The font directory \"" + directory + "\" does not exist");
|
||||||
}
|
}
|
||||||
for (auto& entry : fs::recursive_directory_iterator(directory)) {
|
for (auto& entry : fs::recursive_directory_iterator(directory)) {
|
||||||
std::string ext = entry.path().extension();
|
std::string ext = lower_extension(entry.path());
|
||||||
if (entry.is_regular_file() && (ext == ".ttf" || ext == ".otf")) {
|
if (entry.is_regular_file() && (ext == ".ttf" || ext == ".otf")) {
|
||||||
result.push_back(classify_font_file(entry.path()));
|
result.push_back(classify_font_file(entry.path()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -214,6 +214,7 @@ void Klammer::copy_components(
|
|||||||
}
|
}
|
||||||
for (auto c : cs) {
|
for (auto c : cs) {
|
||||||
m_body[c.target] = c.body;
|
m_body[c.target] = c.body;
|
||||||
|
m_body_generic[c.target] = (c.target == Target_set::general_name);
|
||||||
m_varmap[c.target] = c.varmap;
|
m_varmap[c.target] = c.varmap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -287,6 +288,7 @@ void Klammer::copy_general_klammer_to_undefined(Target_set targets)
|
|||||||
if (m_body.count(target_name) == 0 && target_name != Target_set::declare_name) {
|
if (m_body.count(target_name) == 0 && target_name != Target_set::declare_name) {
|
||||||
// std::cout << " Copying to " << target_name << "\n";
|
// std::cout << " Copying to " << target_name << "\n";
|
||||||
m_body[target_name] = body;
|
m_body[target_name] = body;
|
||||||
|
m_body_generic[target_name] = true; // general body -> writer content
|
||||||
m_defloc[target_name] = loc;
|
m_defloc[target_name] = loc;
|
||||||
m_varmap[target_name] = varmap;
|
m_varmap[target_name] = varmap;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,6 +86,9 @@ public:
|
|||||||
std::map<std::string, Locator> m_defloc {}; // target -> Locator
|
std::map<std::string, Locator> m_defloc {}; // target -> Locator
|
||||||
std::map<std::string, defmode_t> m_defmode {}; // target -> defmode
|
std::map<std::string, defmode_t> m_defmode {}; // target -> defmode
|
||||||
target_variable_map_t m_varmap {}; // target -> map: variable -> index
|
target_variable_map_t m_varmap {}; // target -> map: variable -> index
|
||||||
|
// target -> true if this target's body came from a general ("*") definition
|
||||||
|
// (writer content, subject to target escaping) vs a target-specific one.
|
||||||
|
std::map<std::string, bool> m_body_generic {};
|
||||||
};
|
};
|
||||||
|
|
||||||
std::string klammer_name_from_katom(std::string s, Locator loc);
|
std::string klammer_name_from_katom(std::string s, Locator loc);
|
||||||
|
|||||||
@@ -413,6 +413,66 @@ katom_list Machine::apply_klammer(
|
|||||||
result[i].m_type = katom_t::text;
|
result[i].m_type = katom_t::text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Escape target-specific characters (e.g. tex "&" -> "\&") in the writer
|
||||||
|
// text of a GENERAL klammer's body. Runs BEFORE process_katoms/apply()
|
||||||
|
// below expand the body, so that target-native markup pulled in by nested
|
||||||
|
// klammers (e.g. nl.tex -> "\newline") is left untouched -- only this
|
||||||
|
// klammer's own literal writer text is escaped here; nested klammers escape
|
||||||
|
// theirs when they are applied in turn. Bodies from target-specific
|
||||||
|
// definitions (m_body_generic[target] == false) are already in target form
|
||||||
|
// and skipped. KTESC markers are idempotent, so text already escaped at the
|
||||||
|
// top level passes through unchanged. Two kinds of body content are NOT
|
||||||
|
// writer text and must be skipped:
|
||||||
|
// * ^'...'^ literal spans -- raw target markup the writer typed directly.
|
||||||
|
// At this point they are typed literal_begin/literal_end with plain-text
|
||||||
|
// content (the literal phase runs in process_katoms, below), so track
|
||||||
|
// span depth rather than testing katom type.
|
||||||
|
// * @eval / @read / @cond argument spans -- code, filenames, and
|
||||||
|
// predicates consumed by the primitive, NOT emitted as target text.
|
||||||
|
// (Escaping an underscore in "offer.Price_list(K)" broke @eval.) The
|
||||||
|
// primitive's KLAMMERTEXT result, produced by process_katoms below, is
|
||||||
|
// klammer output and is likewise never escaped -- it is inserted after
|
||||||
|
// this pass and so is untouched, matching the top-level behavior where
|
||||||
|
// @eval is resolved before the escape pass runs.
|
||||||
|
auto gen = klammer.m_body_generic.find(target);
|
||||||
|
if (gen != klammer.m_body_generic.end() && gen->second) {
|
||||||
|
Target tgt = m_targets.get(target, Locator());
|
||||||
|
if (!tgt.m_escapes.empty()) {
|
||||||
|
int literal_depth = 0;
|
||||||
|
int code_depth = 0; // inside an @eval/@read/@cond span
|
||||||
|
std::vector<bool> apply_is_code; // one entry per open application
|
||||||
|
for (auto& k : result) {
|
||||||
|
if (k.m_type == katom_t::literal_begin) { ++literal_depth; continue; }
|
||||||
|
if (k.m_type == katom_t::literal_end) {
|
||||||
|
if (literal_depth > 0) --literal_depth;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (k.m_type == katom_t::eval_begin ||
|
||||||
|
k.m_type == katom_t::read_begin ||
|
||||||
|
k.m_type == katom_t::cond_begin) {
|
||||||
|
apply_is_code.push_back(true);
|
||||||
|
++code_depth;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (k.m_type == katom_t::apply_begin) {
|
||||||
|
apply_is_code.push_back(false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (k.m_type == katom_t::apply_end) {
|
||||||
|
if (!apply_is_code.empty()) {
|
||||||
|
if (apply_is_code.back()) --code_depth;
|
||||||
|
apply_is_code.pop_back();
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (literal_depth == 0 && code_depth == 0 &&
|
||||||
|
(k.m_type == katom_t::text ||
|
||||||
|
k.m_type == katom_t::word ||
|
||||||
|
k.m_type == katom_t::newline))
|
||||||
|
k.m_text = tgt.escape_text(k.m_text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
process_katoms(result, klammer.m_name);
|
process_katoms(result, klammer.m_name);
|
||||||
apply(m_klammers, result, target);
|
apply(m_klammers, result, target);
|
||||||
m_state.close_frame();
|
m_state.close_frame();
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
@@date.k : Date formatted as "16 June 1910" @@
|
@@date.k :days.int 0 :
|
||||||
@@date.html :: @eval time.strftime("%d %B %Y").lstrip('0') eval@ @@
|
Date formatted as "16 June 1910", offset by ^:days days from today
|
||||||
@@date.tex :: @eval time.strftime("%d %B %Y").lstrip('0') eval@ @@
|
(^:days 1 is tomorrow, ^:days -1 is yesterday)
|
||||||
@@date.txt :: @eval time.strftime("%d %B %Y").lstrip('0') eval@ @@
|
@@
|
||||||
|
@@date.html :: @eval date.date(K) eval@ @@
|
||||||
|
@@date.tex :: @eval date.date(K) eval@ @@
|
||||||
|
@@date.txt :: @eval date.date(K) eval@ @@
|
||||||
|
|
||||||
@@datetime.k : Date formatted as "16 June 1910, 13:10 @@
|
@@datetime.k :days.int 0 :
|
||||||
@@datetime.html :: @eval time.strftime("%d %B %Y, %H:%M").lstrip('0') eval@ @@
|
Date and time formatted as "16 June 1910, 13:10", offset by ^:days days
|
||||||
@@datetime.tex :: @eval time.strftime("%d %B %Y, %H:%M").lstrip('0') eval@ @@
|
from today (^:days 1 is tomorrow, ^:days -1 is yesterday)
|
||||||
|
@@
|
||||||
|
@@datetime.html :: @eval date.datetime(K) eval@ @@
|
||||||
|
@@datetime.tex :: @eval date.datetime(K) eval@ @@
|
||||||
|
@@datetime.txt :: @eval date.datetime(K) eval@ @@
|
||||||
|
|
||||||
@@timestamp.k : Date and time formatted as "1910.06.16-12:34" @@
|
@@timestamp.k : Date and time formatted as "1910.06.16-12:34" @@
|
||||||
@@timestamp :: @eval time.strftime("%Y.%m.%d-%H:%M") eval@ @@
|
@@timestamp :: @eval time.strftime("%Y.%m.%d-%H:%M") eval@ @@
|
||||||
|
|||||||
17
sks/date/date.py
Normal file
17
sks/date/date.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Implementation of the @date and @datetime klammers (date.k). Each takes
|
||||||
|
# the current date and time, offset by the :days argument (positive is
|
||||||
|
# future, negative is past), and formats it.
|
||||||
|
|
||||||
|
import datetime as dt
|
||||||
|
|
||||||
|
|
||||||
|
def offset(days):
|
||||||
|
return dt.datetime.now() + dt.timedelta(days=days or 0)
|
||||||
|
|
||||||
|
|
||||||
|
def date(K):
|
||||||
|
return offset(K.days).strftime("%d %B %Y").lstrip("0")
|
||||||
|
|
||||||
|
|
||||||
|
def datetime(K):
|
||||||
|
return offset(K.days).strftime("%d %B %Y, %H:%M").lstrip("0")
|
||||||
@@ -25,10 +25,16 @@ index range "2-" (to the last index), or a name defined by the argument
|
|||||||
commas; each is an index "4", a closed range "1-4", or an open range "6-"
|
commas; each is an index "4", a closed range "1-4", or an open range "6-"
|
||||||
(to the end). All indices are zero-origin.
|
(to the end). All indices are zero-origin.
|
||||||
|
|
||||||
|
A negative index counts from the end, as in Python: -1 is the last index,
|
||||||
|
-2 the second to last, and so on. Ranges are inclusive, so "1--2" is index
|
||||||
|
1 through the second-to-last index (the last index excluded).
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
3 index 3, full extent
|
3 index 3, full extent
|
||||||
2-5(0-2) indices 2 through 5, each restricted to 0 through 2
|
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
|
3(1-4,6-9) index 3, restricted to 1-4 and 6-9
|
||||||
|
-1 the last index
|
||||||
|
1--2 index 1 through the second-to-last index
|
||||||
head(1-) with table hline names: boundary 1, from column 1 on
|
head(1-) with table hline names: boundary 1, from column 1 on
|
||||||
""".strip()
|
""".strip()
|
||||||
|
|
||||||
@@ -38,8 +44,13 @@ class Range_error(Exception):
|
|||||||
super().__init__(f"{message}\n\n{syntax_description}")
|
super().__init__(f"{message}\n\n{syntax_description}")
|
||||||
|
|
||||||
|
|
||||||
item_rgx = re.compile(r"(?:(\d+)(-)?(\d*)|([A-Za-z]+))(?:\(([\d,\-]+)\))?$")
|
# A numeric selector is a signed integer, optionally followed by a range
|
||||||
subset_rgx = re.compile(r"(\d+)(-)?(\d*)$")
|
# part: a separating hyphen and an optional signed end index (empty end =
|
||||||
|
# open range). The leading sign lets an index count from the end (-1 is
|
||||||
|
# the last), matching Python list indexing. The separating hyphen never
|
||||||
|
# collides with a minus sign because \d+ never consumes it.
|
||||||
|
item_rgx = re.compile(r"(?:(-?\d+)(-(-?\d+)?)?|([A-Za-z]+))(?:\(([-\d,]+)\))?$")
|
||||||
|
subset_rgx = re.compile(r"(-?\d+)(-(-?\d+)?)?$")
|
||||||
|
|
||||||
|
|
||||||
def hline_names(count):
|
def hline_names(count):
|
||||||
@@ -49,7 +60,8 @@ def hline_names(count):
|
|||||||
"head": [1],
|
"head": [1],
|
||||||
"bottom": [last],
|
"bottom": [last],
|
||||||
"inner": list(range(1, last)),
|
"inner": list(range(1, last)),
|
||||||
"all": list(range(count))}
|
"all": list(range(count)),
|
||||||
|
"none": []}
|
||||||
|
|
||||||
|
|
||||||
def vline_names(count):
|
def vline_names(count):
|
||||||
@@ -57,7 +69,8 @@ def vline_names(count):
|
|||||||
last = count - 1
|
last = count - 1
|
||||||
return {"outer": [0, last],
|
return {"outer": [0, last],
|
||||||
"inner": list(range(1, last)),
|
"inner": list(range(1, last)),
|
||||||
"all": list(range(count))}
|
"all": list(range(count)),
|
||||||
|
"none": []}
|
||||||
|
|
||||||
|
|
||||||
class Indexed_range:
|
class Indexed_range:
|
||||||
@@ -126,31 +139,36 @@ class Indexed_ranges:
|
|||||||
argument = f"{self.argument} argument: " if self.argument else ""
|
argument = f"{self.argument} argument: " if self.argument else ""
|
||||||
raise Range_error(f"{argument}{message}")
|
raise Range_error(f"{argument}{message}")
|
||||||
|
|
||||||
|
def normalize(self, raw, spec, count):
|
||||||
|
"""Resolve a possibly-negative index to 0..count-1 (Python-style):
|
||||||
|
a negative index counts from the end (-1 is the last)."""
|
||||||
|
i = raw + count if raw < 0 else raw
|
||||||
|
if not 0 <= i < count:
|
||||||
|
self.error(f'In "{spec}", index {raw} is out of range '
|
||||||
|
f"(0 through {count - 1}, or -1 through -{count}).")
|
||||||
|
return i
|
||||||
|
|
||||||
def parse(self, spec):
|
def parse(self, spec):
|
||||||
match = item_rgx.match(spec)
|
match = item_rgx.match(spec)
|
||||||
if not match:
|
if not match:
|
||||||
self.error(f'"{spec}" is not a valid indexed_range.')
|
self.error(f'"{spec}" is not a valid indexed_range.')
|
||||||
number, hyphen, end, name, subsets = match.groups()
|
number, range_part, end, name, subsets = match.groups()
|
||||||
if name is not None:
|
if name is not None:
|
||||||
if name not in self.names:
|
if name not in self.names:
|
||||||
known = " ".join(self.names) or "none"
|
known = " ".join(self.names) or "none"
|
||||||
self.error(f'"{name}" is not a valid name here '
|
self.error(f'"{name}" is not a valid name here '
|
||||||
f"(valid names: {known}).")
|
f"(valid names: {known}).")
|
||||||
indices = self.names[name]
|
indices = self.names[name]
|
||||||
|
elif range_part is None:
|
||||||
|
indices = [self.normalize(int(number), spec, self.count)]
|
||||||
else:
|
else:
|
||||||
start = int(number)
|
first = self.normalize(int(number), spec, self.count)
|
||||||
if not hyphen:
|
last = (self.normalize(int(end), spec, self.count)
|
||||||
indices = [start]
|
if end else self.count - 1)
|
||||||
else:
|
if first > last:
|
||||||
last = int(end) if end else self.count - 1
|
self.error(f'In "{spec}", the index range start {first} '
|
||||||
if start > last:
|
|
||||||
self.error(f'In "{spec}", the index range start {start} '
|
|
||||||
f"is greater than its end {last}.")
|
f"is greater than its end {last}.")
|
||||||
indices = list(range(start, last + 1))
|
indices = list(range(first, 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
|
ranges = self.parse_subsets(spec, subsets) if subsets else None
|
||||||
for i in indices:
|
for i in indices:
|
||||||
entry = self.by_index.setdefault(i, Indexed_range(i, self.maxval))
|
entry = self.by_index.setdefault(i, Indexed_range(i, self.maxval))
|
||||||
@@ -160,23 +178,24 @@ class Indexed_ranges:
|
|||||||
entry.add_ranges(ranges)
|
entry.add_ranges(ranges)
|
||||||
|
|
||||||
def parse_subsets(self, spec, subsets):
|
def parse_subsets(self, spec, subsets):
|
||||||
|
# Subset indices run 0..maxval inclusive, so their count is
|
||||||
|
# maxval + 1 and a negative subset index resolves against it.
|
||||||
|
count = self.maxval + 1
|
||||||
ranges = []
|
ranges = []
|
||||||
for part in subsets.split(","):
|
for part in subsets.split(","):
|
||||||
match = subset_rgx.match(part)
|
match = subset_rgx.match(part)
|
||||||
if not match:
|
if not match:
|
||||||
self.error(f'In "{spec}", "{part}" is not a valid subset.')
|
self.error(f'In "{spec}", "{part}" is not a valid subset.')
|
||||||
number, hyphen, end = match.groups()
|
number, range_part, end = match.groups()
|
||||||
start = int(number)
|
if range_part is None:
|
||||||
if not hyphen:
|
start = last = self.normalize(int(number), spec, count)
|
||||||
last = start
|
|
||||||
else:
|
else:
|
||||||
last = int(end) if end else self.maxval
|
start = self.normalize(int(number), spec, count)
|
||||||
|
last = (self.normalize(int(end), spec, count)
|
||||||
|
if end else self.maxval)
|
||||||
if start > last:
|
if start > last:
|
||||||
self.error(f'In "{spec}", the subset start {start} '
|
self.error(f'In "{spec}", the subset start {start} '
|
||||||
f"is greater than its end {last}.")
|
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])
|
ranges.append([start, last])
|
||||||
return ranges
|
return ranges
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,10 @@
|
|||||||
@@@argtype index_subsets |
|
@@@argtype index_subsets |
|
||||||
one or more subsets in parentheses, attached to an index. Each subset is a
|
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
|
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.
|
the end). A negative index counts from the end (-1 is the last). Several
|
||||||
|
subsets are separated by commas, with no spaces.
|
||||||
Example: (1-4,6-9)
|
Example: (1-4,6-9)
|
||||||
:pattern \((?^:\d+(?^:-\d*)?)(?^:,\d+(?^:-\d*)?)*\)
|
:pattern \((?^:-?\d+(?^:-(?^:-?\d+)?)?)(?^:,-?\d+(?^:-(?^:-?\d+)?)?)*\)
|
||||||
@@@
|
@@@
|
||||||
|
|
||||||
@@@argtype indexed_range |
|
@@@argtype indexed_range |
|
||||||
@@ -13,14 +14,18 @@
|
|||||||
single index <i>, a closed index range <i>-<j>, or an open index range <i>-
|
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
|
(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.
|
(see the index_subsets type) restricting the extent in the other dimension.
|
||||||
All indices are zero-origin. Examples:
|
All indices are zero-origin. A negative index counts from the end, as in
|
||||||
|
Python: -1 is the last index, -2 the second to last. Ranges are inclusive,
|
||||||
|
so "1--2" is index 1 through the second-to-last index. Examples:
|
||||||
|
|
||||||
3 index 3, full extent
|
3 index 3, full extent
|
||||||
2-5 indices 2 through 5, 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
|
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
|
2-5(0-2) indices 2 through 5, each restricted to 0 through 2
|
||||||
|
-1 the last index
|
||||||
|
1--2 index 1 through the second-to-last index
|
||||||
|
|
||||||
:pattern \d+(?^:-\d*)?(?^:'index_subsets')?
|
:pattern -?\d+(?^:-(?^:-?\d+)?)?(?^:'index_subsets')?
|
||||||
@@@
|
@@@
|
||||||
|
|
||||||
@@@argtype column_width |
|
@@@argtype column_width |
|
||||||
@@ -51,16 +56,19 @@
|
|||||||
is either a boundary name or an indexed_range of boundary indices. The
|
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),
|
names are 'top' (boundary 0), 'head' (boundary 1, under a header row),
|
||||||
'bottom' (boundary N), 'inner' (all boundaries between top and bottom),
|
'bottom' (boundary N), 'inner' (all boundaries between top and bottom),
|
||||||
and 'all' (every boundary). A name or index may be followed by
|
'all' (every boundary), and 'none' (no lines). A name or index may be
|
||||||
parenthesized subsets to draw only part of a line, given as zero-origin
|
followed by parenthesized subsets to draw only part of a line, given as
|
||||||
column ranges. Examples:
|
zero-origin column ranges. When ^:hline is given it replaces the default
|
||||||
|
lines (with a header, 'head' and 'bottom'); ^:hline none removes them.
|
||||||
|
Examples:
|
||||||
|
|
||||||
top bottom lines above and below the table
|
top bottom lines above and below the table
|
||||||
head(1-) a line under the header, from column 1 to the last
|
head(1-) a line under the header, from column 1 to the last
|
||||||
3(1-4,6-9) two partial lines at boundary 3
|
3(1-4,6-9) two partial lines at boundary 3
|
||||||
all every line
|
all every line
|
||||||
|
none no lines
|
||||||
|
|
||||||
:pattern ((?^:top^|head^|inner^|bottom^|all)(?^:'index_subsets')?^|'indexed_range'^|\s+)+
|
:pattern ((?^:top^|head^|inner^|bottom^|all^|none)(?^:'index_subsets')?^|'indexed_range'^|\s+)+
|
||||||
:python_cast (lambda s : s.split())
|
:python_cast (lambda s : s.split())
|
||||||
@@@
|
@@@
|
||||||
|
|
||||||
@@ -71,15 +79,15 @@
|
|||||||
the left; boundary i lies to the left of column i, and boundary M is the
|
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
|
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'
|
boundary indices. The names are 'outer' (boundaries 0 and M), 'inner'
|
||||||
(all boundaries between them), and 'all' (every boundary). A name or
|
(all boundaries between them), 'all' (every boundary), and 'none' (no
|
||||||
index may be followed by parenthesized subsets to draw only part of a
|
lines). A name or index may be followed by parenthesized subsets to
|
||||||
line, given as zero-origin row ranges. Examples:
|
draw only part of a line, given as zero-origin row ranges. Examples:
|
||||||
|
|
||||||
outer lines at the left and right edges
|
outer lines at the left and right edges
|
||||||
2(0-3) a line left of column 2, spanning rows 0 through 3
|
2(0-3) a line left of column 2, spanning rows 0 through 3
|
||||||
all every line
|
all every line
|
||||||
|
|
||||||
:pattern ((?^:outer^|inner^|all)(?^:'index_subsets')?^|'indexed_range'^|\s+)+
|
:pattern ((?^:outer^|inner^|all^|none)(?^:'index_subsets')?^|'indexed_range'^|\s+)+
|
||||||
:python_cast (lambda s : s.split())
|
:python_cast (lambda s : s.split())
|
||||||
@@@
|
@@@
|
||||||
|
|
||||||
@@ -105,22 +113,35 @@
|
|||||||
|
|
||||||
<target> = <operator> <operand> <operand> ...
|
<target> = <operator> <operand> <operand> ...
|
||||||
|
|
||||||
where the target is a single cell written <row>(<column>) with zero-origin
|
where the operator is one of + - * / and each operand is a cell selection
|
||||||
indices, the operator is one of + - * /, and each operand is either a cell
|
or a number. A cell selection is an indexed_range read as <rows>(<columns>);
|
||||||
selection or a number. A cell selection is an indexed_range read as
|
a range expands to all of its cells in row order, so "+ 1-2(3)" sums column 3
|
||||||
<rows>(<columns>); a range expands to all of its cells in row order, so
|
of rows 1 and 2. The SHAPE of the target chooses the operation: a single
|
||||||
"+ 1-2(3)" sums column 3 of rows 1 and 2. A plain number is a constant
|
cell <row>(<column>) folds the operands to one value, while a target that
|
||||||
|
ranges over rows (0-(2)) or columns (-1(0-)) runs the calculation once per
|
||||||
|
row or column (a "map"). See notes/calc_notation.md for the map forms,
|
||||||
|
relative operands, and broadcasting. A plain number is a constant
|
||||||
and always uses a period as its decimal mark. Operators fold from the
|
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
|
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
|
and / gives the reciprocal. Calculations run in the order given, and each
|
||||||
reads the values earlier calculations have written, as displayed.
|
reads the values earlier calculations have written, as displayed.
|
||||||
Example:
|
Negative indices count from the end (see indexed_range), which is handy
|
||||||
|
when a total sits in the last row: "-1(5) = + 1--2(5)" writes into the
|
||||||
|
last row of column 5 the sum of that column from row 1 through the row
|
||||||
|
above it. Example:
|
||||||
|
|
||||||
1(3) = * 1(1-2) ;
|
1(3) = * 1(1-2) ;
|
||||||
2(3) = * 2(1-2) ;
|
2(3) = * 2(1-2) ;
|
||||||
3(3) = + 1-2(3)
|
3(3) = + 1-2(3)
|
||||||
|
|
||||||
:pattern \s*(\d+\(\d+\)\s*=\s*[-+*/](\s+(\d+(?^:-\d*)?'index_subsets'^|'float'))+\s*(;\s*^|\s*$))+
|
# Coarse structural check only -- "<target> = <op> <operand>..." groups
|
||||||
|
# separated by ";" -- so that a malformed target or operand reaches
|
||||||
|
# run_calc() in table.py, whose per-token validation gives a precise message
|
||||||
|
# (e.g. an infix "* a * b" reports that "*" is not a number or a cell
|
||||||
|
# selection) instead of this whole description being dumped. A token is any
|
||||||
|
# run of characters other than space, ";", or "=" ("^^" escapes the regex
|
||||||
|
# class negation "[^...]" so the Klammertext "^" is not consumed).
|
||||||
|
:pattern \s*([^^\s;=]+\s*=\s*[-+*/](\s+[^^\s;=]+)+\s*(;\s*^|\s*$))+
|
||||||
|
|
||||||
@@@
|
@@@
|
||||||
|
|
||||||
@@ -133,10 +154,33 @@
|
|||||||
:default period
|
:default period
|
||||||
@@@
|
@@@
|
||||||
|
|
||||||
@@@argtype format_spec |
|
@@@argtype table_hpos |
|
||||||
a Python format specification applied to calculated cell values, for
|
cell position overrides, as one or more <cells> <position> pairs
|
||||||
example ",.2f" for two decimal places with grouped thousands.
|
separated by semicolons (the same list style as ^:calc). <cells> is an
|
||||||
:pattern \S+
|
indexed_range selecting cells; <position> is l, c, or r and overrides
|
||||||
|
the column position given by ^:cell_hpos for those cells. A colspan
|
||||||
|
anchor's override positions the whole merged cell. For example,
|
||||||
|
"-3--1(3) r" right-justifies the cells in column 3 of the last three
|
||||||
|
rows.
|
||||||
|
# Coarse check ("<cells> <position>" pairs); hpos_overrides() in
|
||||||
|
# table.py validates the range and position.
|
||||||
|
:pattern \s*([^^\s;]+\s+[lcr]\s*(;\s*^|\s*$))+
|
||||||
|
@@@
|
||||||
|
|
||||||
|
@@@argtype table_format |
|
||||||
|
cell formatting, as one or more <cells> <function> pairs separated by
|
||||||
|
semicolons (the same list style as :calc). <cells> is an indexed_range
|
||||||
|
selecting the cells to format; <function> is a <module>.<function> Python
|
||||||
|
reference (the same form the eval klammer uses) to a function that takes the
|
||||||
|
cell's value and the target and returns the formatted text. The function
|
||||||
|
lives in a module the writer supplies (a currency style is specific to a
|
||||||
|
document, so the SKS ships none): for example, with a euro() function in
|
||||||
|
your own module, "0-(5) myformats.euro" formats every cell of column 5 as a
|
||||||
|
Euro amount. Runs after :calc, so it formats computed values; a cell whose
|
||||||
|
value is not a number is left unchanged, with a warning.
|
||||||
|
# Coarse check ("<cells> <function>" pairs); apply_formats() in table.py
|
||||||
|
# validates the range and calls the function.
|
||||||
|
:pattern \s*([^^\s;]+\s+[^^\s;]+\s*(;\s*^|\s*$))+
|
||||||
@@@
|
@@@
|
||||||
|
|
||||||
@@rowcolor.tex s : \colorrow{*s*} @@
|
@@rowcolor.tex s : \colorrow{*s*} @@
|
||||||
@@ -153,12 +197,13 @@
|
|||||||
:vline.table_vline
|
:vline.table_vline
|
||||||
:grid.bool false
|
:grid.bool false
|
||||||
:cell_hpos.cell_hpos
|
:cell_hpos.cell_hpos
|
||||||
|
:hpos.table_hpos
|
||||||
:header_font.font i
|
:header_font.font i
|
||||||
:font.font_list
|
:font.font_list
|
||||||
:colspan.table_span
|
:colspan.table_span
|
||||||
:rowspan.table_span
|
:rowspan.table_span
|
||||||
:calc.table_calc
|
:calc.table_calc
|
||||||
:calc_format.format_spec
|
:format.table_format
|
||||||
:decimal.decimal_mark
|
:decimal.decimal_mark
|
||||||
:leading.float 1.3
|
:leading.float 1.3
|
||||||
:colsep 4pt
|
:colsep 4pt
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import functools
|
import functools
|
||||||
import collections
|
import collections
|
||||||
|
import importlib
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
@@ -14,6 +15,15 @@ from indexed_range import Indexed_ranges, hline_names, vline_names
|
|||||||
import table_cell
|
import table_cell
|
||||||
import font
|
import font
|
||||||
|
|
||||||
|
# ---- :format functions ---------------------------------------------------
|
||||||
|
# A :format function takes (value, target) and returns the formatted cell
|
||||||
|
# text. It is named <module>.<function> in the :format list (like an eval
|
||||||
|
# reference), so it can live in ANY module: the SKS ships none by default, and
|
||||||
|
# a document defines its own (e.g. a euro() in the document's .py, named
|
||||||
|
# "<module>.euro" in :format) because a specific currency style is a property
|
||||||
|
# of that document, not of the SKS. A function should emit period-decimal
|
||||||
|
# numbers; apply_formats() applies the :decimal comma swap.
|
||||||
|
|
||||||
def extend(lst, count, fill=None):
|
def extend(lst, count, fill=None):
|
||||||
if isinstance(lst, str):
|
if isinstance(lst, str):
|
||||||
lst = lst.split()
|
lst = lst.split()
|
||||||
@@ -44,18 +54,18 @@ class Table(klammer_base.Klammer_base):
|
|||||||
id = 0
|
id = 0
|
||||||
def __init__(self, K):
|
def __init__(self, K):
|
||||||
super().__init__(K)
|
super().__init__(K)
|
||||||
|
self.row_count = len(self.rows)
|
||||||
if self.grid:
|
if self.grid:
|
||||||
self.vline = ["all"]
|
self.vline = ["all"]
|
||||||
self.hline = ["all"]
|
self.hline = ["all"]
|
||||||
self.row_count = len(self.rows)
|
elif not self.hline and self.header:
|
||||||
if self.header:
|
# Default lines: under the header row and at the bottom. A
|
||||||
self.hline += ["1", str(self.row_count)]
|
# writer's own :hline replaces them (":hline none" = no lines).
|
||||||
|
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])
|
||||||
# Rows with fewer cells than the widest row are padded with empty
|
# Rows with fewer cells than the widest row are padded with empty
|
||||||
# cells (last-value duplication is for argument lists, not content).
|
# cells (last-value duplication is for argument lists, not content).
|
||||||
self.rows = [row + [""] * (self.row_size - len(row)) for row in self.rows]
|
self.rows = [row + [""] * (self.row_size - len(row)) for row in self.rows]
|
||||||
if self.calc:
|
|
||||||
self.calculate()
|
|
||||||
self.s_vline = Indexed_ranges(self.row_size + 1, self.row_count - 1, self.vline,
|
self.s_vline = Indexed_ranges(self.row_size + 1, self.row_count - 1, self.vline,
|
||||||
vline_names(self.row_size + 1), ":vline")
|
vline_names(self.row_size + 1), ":vline")
|
||||||
self.s_hline = Indexed_ranges(self.row_count + 1, self.row_size - 1, self.hline,
|
self.s_hline = Indexed_ranges(self.row_count + 1, self.row_size - 1, self.hline,
|
||||||
@@ -64,25 +74,58 @@ class Table(klammer_base.Klammer_base):
|
|||||||
argument=":rowspan")
|
argument=":rowspan")
|
||||||
self.s_colspan = Indexed_ranges(self.row_count, self.row_size - 1, self.colspan,
|
self.s_colspan = Indexed_ranges(self.row_count, self.row_size - 1, self.colspan,
|
||||||
argument=":colspan")
|
argument=":colspan")
|
||||||
|
# :calc runs after the span structures exist so calculate() can warn
|
||||||
|
# when a target lands in a cell hidden by a colspan/rowspan merge;
|
||||||
|
# :format runs after :calc so it formats the computed values.
|
||||||
|
if self.calc:
|
||||||
|
self.compute_coverage()
|
||||||
|
self.calculate()
|
||||||
|
if self.format:
|
||||||
|
self.apply_formats()
|
||||||
self.cell_hpos = extend(parse_hpos(self.K_target, self.cell_hpos), self.row_size)
|
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.hpos_map = self.hpos_overrides() if self.hpos else {}
|
||||||
self.make_cells(self.rows)
|
self.make_cells(self.rows)
|
||||||
|
|
||||||
# Calculated cell values (:calc). Calculations run in the order given;
|
# Calculated cell values (:calc). A calculation is
|
||||||
# each reads cell values as displayed (display-precision semantics), so
|
# <target> = <op> <operand> ... (prefix operator: + - * /)
|
||||||
# a printed total always equals the sum of the printed lines. Only
|
# and the SHAPE of the target selects the operation:
|
||||||
# calculation targets are formatted (:calc_format); other cells keep
|
# * single cell r(c) -> FOLD: the operands collapse to one value.
|
||||||
# their authored text. Future operators to consider: min, max, mean,
|
# * row range R(c) -> horizontal MAP: run once per row in R.
|
||||||
# and a per-calculation format override.
|
# * column range r(C) -> vertical MAP: run once per column in C.
|
||||||
|
# In a map the target's ranged axis iterates; an operand's aligned axis
|
||||||
|
# iterates in lockstep and any other range in it folds. A relative
|
||||||
|
# operand omits the iterated axis ("(col)" in a row map; bare "rows" in a
|
||||||
|
# column map); a constant or a single fixed cell broadcasts. See
|
||||||
|
# notes/calc_notation.md. Calculations run in order, each reading values
|
||||||
|
# as displayed (display-precision); :format styles the results.
|
||||||
|
# Future operators to consider: min, max, mean.
|
||||||
|
|
||||||
calc_target_rgx = re.compile(r"(\d+)\((\d+)\)$")
|
# A single-axis selector: an index or an inclusive range; negatives count
|
||||||
calc_cell_rgx = re.compile(r"\d+(-\d*)?\(")
|
# from the end (-1 = last). A target/operand token is <rows>(<cols>),
|
||||||
|
# <rows>, or (<cols>) -- the last two are the relative operand forms.
|
||||||
|
selector_rgx = re.compile(r"^(-?\d+)(-(-?\d+)?)?$")
|
||||||
|
operand_rgx = re.compile(
|
||||||
|
r"^(?P<rows>-?\d+(?:-(?:-?\d+)?)?)?(?:\((?P<cols>[-\d,]+)\))?$")
|
||||||
|
|
||||||
def calc_error(self, calc, message):
|
def calc_error(self, calc, message):
|
||||||
raise Exception(f'In the :calc calculation "{calc}": {message}')
|
raise Exception(f'In the :calc calculation "{calc}": {message}')
|
||||||
|
|
||||||
def parse_number(self, text, ref, calc):
|
def calc_warn(self, calc, message):
|
||||||
|
# Non-fatal: the value is still computed and stored (a covered target
|
||||||
|
# may be read as an operand by a later calc), it just is not rendered.
|
||||||
|
print(f'Warning: in the :calc calculation "{calc}": {message}',
|
||||||
|
file=sys.stderr)
|
||||||
|
|
||||||
|
def selector_error(self, ctx, message):
|
||||||
|
# ctx is the caller's prefix, e.g. 'In the :calc calculation "..."' or
|
||||||
|
# 'In :format "..."', so the same selector parser serves both.
|
||||||
|
raise Exception(f"{ctx}: {message}")
|
||||||
|
|
||||||
|
def to_number(self, text):
|
||||||
|
# A displayed cell value as a float, honoring :decimal and thousands
|
||||||
|
# separators; None (no error raised) when it is not a number.
|
||||||
s = text.strip()
|
s = text.strip()
|
||||||
if self.decimal == "comma":
|
if self.decimal == "comma":
|
||||||
s = s.translate(str.maketrans(",.", ".,"))
|
s = s.translate(str.maketrans(",.", ".,"))
|
||||||
@@ -90,44 +133,129 @@ class Table(klammer_base.Klammer_base):
|
|||||||
try:
|
try:
|
||||||
return float(s)
|
return float(s)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self.calc_error(
|
return None
|
||||||
calc, f'the cell {ref} contains "{text.strip()}", '
|
|
||||||
"which is not a number")
|
|
||||||
|
|
||||||
def format_number(self, value, calc):
|
def parse_number(self, text, ref, calc):
|
||||||
if self.calc_format:
|
v = self.to_number(text)
|
||||||
try:
|
if v is None:
|
||||||
s = format(value, self.calc_format)
|
# Unescape KTESC markers so the message shows the character the
|
||||||
except ValueError:
|
# writer typed (e.g. "$5") rather than "KTESC0024KTESC5".
|
||||||
self.calc_error(
|
self.calc_error(
|
||||||
calc, f'"{self.calc_format}" is not a valid '
|
calc, "the cell {} contains \"{}\", which is not a number"
|
||||||
"format specification")
|
.format(ref, klammer_base.unescape_ktesc(text.strip())))
|
||||||
elif value.is_integer():
|
return v
|
||||||
s = str(int(value))
|
|
||||||
else:
|
def format_number(self, value):
|
||||||
s = str(value)
|
# Calc results are stored as plain numbers; :format does any styling.
|
||||||
|
s = str(int(value)) if value.is_integer() else str(value)
|
||||||
if self.decimal == "comma":
|
if self.decimal == "comma":
|
||||||
s = s.translate(str.maketrans(",.", ".,"))
|
s = s.translate(str.maketrans(",.", ".,"))
|
||||||
return s
|
return s
|
||||||
|
|
||||||
def operand_values(self, token, calc):
|
def calc_norm(self, i, count, spec, ctx):
|
||||||
# A token with subsets is a cell selection; a bare number is a
|
# Resolve a possibly-negative index; -1 is the last, like Python.
|
||||||
# constant (always period-decimal, independent of :decimal).
|
j = i + count if i < 0 else i
|
||||||
if not self.calc_cell_rgx.match(token):
|
if not 0 <= j < count:
|
||||||
return [float(token)]
|
self.selector_error(
|
||||||
selection = Indexed_ranges(self.row_count, self.row_size - 1,
|
ctx, f'in "{spec}", index {i} is out of range '
|
||||||
[token], argument=":calc")
|
f"(0 through {count - 1})")
|
||||||
values = []
|
return j
|
||||||
for row_i in selection.by_index:
|
|
||||||
for _, col_i in selection.by_index[row_i].items():
|
def calc_selectors(self, spec, count, ctx):
|
||||||
values.append(self.parse_number(
|
# "3", "0-2", "0--2", "-1", or a comma-separated list of those, to a
|
||||||
self.rows[row_i][col_i], f"{row_i}({col_i})", calc))
|
# list of indices in written order (order matters for a left fold).
|
||||||
return values
|
result = []
|
||||||
|
for part in spec.split(","):
|
||||||
|
m = self.selector_rgx.match(part)
|
||||||
|
if not m:
|
||||||
|
self.selector_error(ctx, f'"{part}" is not a valid selector')
|
||||||
|
start = self.calc_norm(int(m.group(1)), count, part, ctx)
|
||||||
|
if m.group(2) is None:
|
||||||
|
result.append(start)
|
||||||
|
continue
|
||||||
|
end = (self.calc_norm(int(m.group(3)), count, part, ctx)
|
||||||
|
if m.group(3) else count - 1)
|
||||||
|
if start > end:
|
||||||
|
self.selector_error(
|
||||||
|
ctx, f'in "{part}", the start {start} is after the end {end}')
|
||||||
|
result += list(range(start, end + 1))
|
||||||
|
return result
|
||||||
|
|
||||||
|
def cell_number(self, r, c, calc):
|
||||||
|
return self.parse_number(self.rows[r][c], f"{r}({c})", calc)
|
||||||
|
|
||||||
|
def parse_operand(self, token, calc):
|
||||||
|
# ('const', value) or ('cells', rows, cols) where each of rows/cols is
|
||||||
|
# a list of indices, or None when that axis is not written (a relative
|
||||||
|
# operand, resolved against the target's iterated axis by operand_cells).
|
||||||
|
try:
|
||||||
|
return ('const', float(token))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
m = self.operand_rgx.match(token)
|
||||||
|
if not m or (m.group('rows') is None and m.group('cols') is None):
|
||||||
|
self.calc_error(
|
||||||
|
calc, f'"{token}" is not a number or a cell selection')
|
||||||
|
ctx = f'In the :calc calculation "{calc}"'
|
||||||
|
rows = (self.calc_selectors(m.group('rows'), self.row_count, ctx)
|
||||||
|
if m.group('rows') is not None else None)
|
||||||
|
cols = (self.calc_selectors(m.group('cols'), self.row_size, ctx)
|
||||||
|
if m.group('cols') is not None else None)
|
||||||
|
return ('cells', rows, cols)
|
||||||
|
|
||||||
|
def operand_cells(self, token, calc, mode, index, trange):
|
||||||
|
# The operand's numbers for the current target cell. mode is 'scalar',
|
||||||
|
# 'row' (horizontal map, rows iterate), or 'col' (vertical, cols
|
||||||
|
# iterate); index is the current row/col; trange is the target's range.
|
||||||
|
kind = self.parse_operand(token, calc)
|
||||||
|
if kind[0] == 'const':
|
||||||
|
return [kind[1]]
|
||||||
|
_, rows, cols = kind
|
||||||
|
if mode == 'scalar':
|
||||||
|
if rows is None or cols is None:
|
||||||
|
self.calc_error(
|
||||||
|
calc, f'"{token}" is a relative operand; it needs a ranged '
|
||||||
|
"target (a row or column range) to resolve against")
|
||||||
|
return [self.cell_number(r, c, calc) for r in rows for c in cols]
|
||||||
|
if mode == 'row': # rows iterate; any columns fold
|
||||||
|
if cols is None:
|
||||||
|
self.calc_error(
|
||||||
|
calc, f'"{token}" selects no column; a row-map operand '
|
||||||
|
"names a column, e.g. (0) or 0-(0)")
|
||||||
|
if rows is None: # relative: this row
|
||||||
|
use_rows = [index]
|
||||||
|
elif len(rows) == 1: # a fixed row broadcasts
|
||||||
|
use_rows = rows
|
||||||
|
elif rows == trange: # explicit range in lockstep
|
||||||
|
use_rows = [index]
|
||||||
|
else:
|
||||||
|
self.calc_error(
|
||||||
|
calc, f'the rows of "{token}" must match the target rows')
|
||||||
|
return [self.cell_number(r, c, calc) for r in use_rows for c in cols]
|
||||||
|
# mode == 'col': columns iterate; any rows fold
|
||||||
|
if rows is None:
|
||||||
|
self.calc_error(
|
||||||
|
calc, f'"{token}" selects no row; a column-map operand names '
|
||||||
|
"rows, e.g. 0--2 or 0--2(0-)")
|
||||||
|
if cols is None: # relative: this column
|
||||||
|
use_cols = [index]
|
||||||
|
elif len(cols) == 1: # a fixed column broadcasts
|
||||||
|
use_cols = cols
|
||||||
|
elif cols == trange: # explicit range in lockstep
|
||||||
|
use_cols = [index]
|
||||||
|
else:
|
||||||
|
self.calc_error(
|
||||||
|
calc, f'the columns of "{token}" must match the target columns')
|
||||||
|
return [self.cell_number(r, c, calc) for r in rows for c in use_cols]
|
||||||
|
|
||||||
def apply_operator(self, op, values, calc):
|
def apply_operator(self, op, values, calc):
|
||||||
if len(values) == 1: # Lisp-style unary - and /
|
if len(values) == 1: # Lisp-style unary: - negates, / reciprocates
|
||||||
return {"+": values[0], "*": values[0],
|
v = values[0] # + and * of one operand are the operand itself
|
||||||
"-": -values[0], "/": 1 / values[0]}[op]
|
if op == "-":
|
||||||
|
return -v
|
||||||
|
if op == "/": # compute 1/v only for "/", so "+ <zero cell>"
|
||||||
|
return 1 / v # does not raise a spurious ZeroDivisionError
|
||||||
|
return v
|
||||||
result = values[0]
|
result = values[0]
|
||||||
for v in values[1:]: # Fold from the left
|
for v in values[1:]: # Fold from the left
|
||||||
if op == "+":
|
if op == "+":
|
||||||
@@ -140,31 +268,118 @@ class Table(klammer_base.Klammer_base):
|
|||||||
result /= v
|
result /= v
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def calculate(self):
|
def calc_fold(self, op, operands, calc, mode, index, trange):
|
||||||
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 = []
|
values = []
|
||||||
for token in tokens[1:]:
|
for token in operands:
|
||||||
values += self.operand_values(token, calc)
|
values += self.operand_cells(token, calc, mode, index, trange)
|
||||||
try:
|
try:
|
||||||
result = self.apply_operator(tokens[0], values, calc)
|
return self.apply_operator(op, values, calc)
|
||||||
except ZeroDivisionError:
|
except ZeroDivisionError:
|
||||||
self.calc_error(calc, "division by zero")
|
self.calc_error(calc, "division by zero")
|
||||||
self.rows[row_i][col_i] = self.format_number(result, calc)
|
|
||||||
|
def calc_assign(self, r, c, value, calc, target_text):
|
||||||
|
if (r, c) in self.covered:
|
||||||
|
self.calc_warn(
|
||||||
|
calc, f"the target {target_text} is a cell hidden by a colspan "
|
||||||
|
"or rowspan merge; its computed value will not be shown")
|
||||||
|
self.rows[r][c] = self.format_number(value)
|
||||||
|
|
||||||
|
def calculate(self):
|
||||||
|
for calc in [c.strip() for c in self.calc.split(";") if c.strip()]:
|
||||||
|
self.run_calc(calc)
|
||||||
|
|
||||||
|
def run_calc(self, calc):
|
||||||
|
target, eq, expression = calc.partition("=")
|
||||||
|
target = target.strip()
|
||||||
|
m = self.operand_rgx.match(target)
|
||||||
|
if not eq or not m or m.group('rows') is None or m.group('cols') is None:
|
||||||
|
self.calc_error(
|
||||||
|
calc, "the target must be a cell r(c) or a ranged cell such as "
|
||||||
|
"0-(2) or -1(0-), followed by \"=\"")
|
||||||
|
ctx = f'In the :calc calculation "{calc}"'
|
||||||
|
trows = self.calc_selectors(m.group('rows'), self.row_count, ctx)
|
||||||
|
tcols = self.calc_selectors(m.group('cols'), self.row_size, ctx)
|
||||||
|
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")
|
||||||
|
op, operands = tokens[0], tokens[1:]
|
||||||
|
row_range, col_range = len(trows) > 1, len(tcols) > 1
|
||||||
|
if row_range and col_range:
|
||||||
|
self.calc_error(
|
||||||
|
calc, "the target may range over rows or columns, but not both")
|
||||||
|
if not row_range and not col_range: # single cell: a fold
|
||||||
|
v = self.calc_fold(op, operands, calc, 'scalar', None, None)
|
||||||
|
self.calc_assign(trows[0], tcols[0], v, calc, target)
|
||||||
|
elif row_range: # horizontal map
|
||||||
|
for r in trows:
|
||||||
|
v = self.calc_fold(op, operands, calc, 'row', r, trows)
|
||||||
|
self.calc_assign(r, tcols[0], v, calc, target)
|
||||||
|
else: # vertical map
|
||||||
|
for c in tcols:
|
||||||
|
v = self.calc_fold(op, operands, calc, 'col', c, tcols)
|
||||||
|
self.calc_assign(trows[0], c, v, calc, target)
|
||||||
|
|
||||||
|
# ---- :format ----------------------------------------------------------
|
||||||
|
#
|
||||||
|
# ";"-separated <cells> <function> pairs (same list style as :calc).
|
||||||
|
# <cells> is an indexed_range; <function> is a "<module>.<function>"
|
||||||
|
# reference (like an @eval reference) to a Python function taking
|
||||||
|
# (value, target) and returning the formatted cell text. Each selected
|
||||||
|
# cell's value is parsed as a number (honoring :decimal); if numeric the
|
||||||
|
# function is called and its result -- with the :decimal comma swap
|
||||||
|
# applied -- replaces the cell (e.g. a writer's myformats.euro function
|
||||||
|
# turns 1234.56 into "1,234.56 €"). A
|
||||||
|
# non-numeric cell is left as-is with a warning. Runs AFTER :calc. The
|
||||||
|
# result is inserted verbatim (this pass is after the cell-processing pass),
|
||||||
|
# so a function may emit target markup directly.
|
||||||
|
|
||||||
|
def format_warn(self, message):
|
||||||
|
print(f"Warning: in :format: {message}", file=sys.stderr)
|
||||||
|
|
||||||
|
def format_function(self, spec, ctx):
|
||||||
|
# Resolve "<module>.<function>" to a callable, like an @eval reference.
|
||||||
|
if "." not in spec:
|
||||||
|
self.selector_error(
|
||||||
|
ctx, f'the format function "{spec}" must be written '
|
||||||
|
"<module>.<function>, e.g. table.euro")
|
||||||
|
mod_name, func_name = spec.rsplit(".", 1)
|
||||||
|
try:
|
||||||
|
return getattr(importlib.import_module(mod_name), func_name)
|
||||||
|
except (ImportError, AttributeError):
|
||||||
|
self.selector_error(
|
||||||
|
ctx, f'the format function "{spec}" was not found')
|
||||||
|
|
||||||
|
def apply_formats(self):
|
||||||
|
for stmt in [s.strip() for s in self.format.split(";") if s.strip()]:
|
||||||
|
ctx = f'In :format "{stmt}"'
|
||||||
|
parts = stmt.split()
|
||||||
|
if len(parts) != 2:
|
||||||
|
self.selector_error(
|
||||||
|
ctx, "each entry is <cells> <function>, e.g. 0-(5) myformats.euro")
|
||||||
|
rangespec, spec = parts
|
||||||
|
func = self.format_function(spec, ctx)
|
||||||
|
m = self.operand_rgx.match(rangespec)
|
||||||
|
if not m or m.group('rows') is None or m.group('cols') is None:
|
||||||
|
self.selector_error(
|
||||||
|
ctx, f'"{rangespec}" is not a cell range like 0-(5) '
|
||||||
|
"or 1--2(0-3)")
|
||||||
|
rows = self.calc_selectors(m.group('rows'), self.row_count, ctx)
|
||||||
|
cols = self.calc_selectors(m.group('cols'), self.row_size, ctx)
|
||||||
|
for r in rows:
|
||||||
|
for c in cols:
|
||||||
|
v = self.to_number(self.rows[r][c])
|
||||||
|
if v is None:
|
||||||
|
self.format_warn(
|
||||||
|
f'cell {r}({c}) contains "'
|
||||||
|
f'{klammer_base.unescape_ktesc(self.rows[r][c].strip())}'
|
||||||
|
'", which is not a number; left unformatted')
|
||||||
|
continue
|
||||||
|
result = func(v, self.K_target)
|
||||||
|
if self.decimal == "comma":
|
||||||
|
result = result.translate(str.maketrans(",.", ".,"))
|
||||||
|
self.rows[r][c] = result
|
||||||
|
|
||||||
def span_count(self, spans, index, cross_i):
|
def span_count(self, spans, index, cross_i):
|
||||||
# The count of cells merged by a span anchored at (index, cross_i):
|
# The count of cells merged by a span anchored at (index, cross_i):
|
||||||
@@ -192,16 +407,6 @@ class Table(klammer_base.Klammer_base):
|
|||||||
self.rowspan_covered.add((row_i, col_i))
|
self.rowspan_covered.add((row_i, col_i))
|
||||||
self.covered = self.colspan_covered | self.rowspan_covered
|
self.covered = self.colspan_covered | self.rowspan_covered
|
||||||
|
|
||||||
def remove_redundant_borders(self):
|
|
||||||
remove_right = []
|
|
||||||
for row_i in range(self.row_count):
|
|
||||||
for cell_i in range(self.row_size):
|
|
||||||
a = self.cells[row_i][cell_i]
|
|
||||||
b = self.cells[row_i][cell_i+1]
|
|
||||||
if a.border.right and b.border.left:
|
|
||||||
a.border.right = 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
|
# 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
|
# tex target. The longest cell's font is applied, so a bold or
|
||||||
@@ -225,6 +430,36 @@ class Table(klammer_base.Klammer_base):
|
|||||||
longest = font.tex_fontify(longest, longest_font, 1.0)
|
longest = font.tex_fontify(longest, longest_font, 1.0)
|
||||||
self.column_widths.append(longest)
|
self.column_widths.append(longest)
|
||||||
|
|
||||||
|
# ---- :hpos -------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# ";"-separated <cells> <position> pairs (the same list style as :calc
|
||||||
|
# and :format). <cells> is an indexed_range; <position> is l, c, or r
|
||||||
|
# and overrides the column's :cell_hpos for the selected cells. A
|
||||||
|
# colspan anchor's override positions the whole merged cell; in the tex
|
||||||
|
# target an ordinary overridden cell is wrapped in \multicolumn{1}.
|
||||||
|
|
||||||
|
def hpos_overrides(self):
|
||||||
|
result = {}
|
||||||
|
for stmt in [s.strip() for s in self.hpos.split(";") if s.strip()]:
|
||||||
|
ctx = f'In :hpos "{stmt}"'
|
||||||
|
parts = stmt.split()
|
||||||
|
if len(parts) != 2 or parts[1] not in ("l", "c", "r"):
|
||||||
|
self.selector_error(
|
||||||
|
ctx, "each entry is <cells> <position>, the position one "
|
||||||
|
"of l, c, or r -- e.g. -3--1(3) r")
|
||||||
|
rangespec, pos = parts
|
||||||
|
m = self.operand_rgx.match(rangespec)
|
||||||
|
if not m or m.group('rows') is None or m.group('cols') is None:
|
||||||
|
self.selector_error(
|
||||||
|
ctx, f'"{rangespec}" is not a cell range like 0-(5) '
|
||||||
|
"or 1--2(0-3)")
|
||||||
|
rows = self.calc_selectors(m.group('rows'), self.row_count, ctx)
|
||||||
|
cols = self.calc_selectors(m.group('cols'), self.row_size, ctx)
|
||||||
|
for r in rows:
|
||||||
|
for c in cols:
|
||||||
|
result[(r, c)] = pos
|
||||||
|
return result
|
||||||
|
|
||||||
def make_cells(self, rows):
|
def make_cells(self, rows):
|
||||||
self.compute_coverage()
|
self.compute_coverage()
|
||||||
result = []
|
result = []
|
||||||
@@ -241,10 +476,11 @@ class Table(klammer_base.Klammer_base):
|
|||||||
# boundary at the END of the merged region.
|
# boundary at the END of the merged region.
|
||||||
right_i = cell_i + max(cspan, 1)
|
right_i = cell_i + max(cspan, 1)
|
||||||
bottom_i = row_i + max(rspan, 1)
|
bottom_i = row_i + max(rspan, 1)
|
||||||
|
hpos = self.hpos_map.get((row_i, cell_i), self.cell_hpos[cell_i])
|
||||||
row_cells.append(
|
row_cells.append(
|
||||||
table_cell.Cell(
|
table_cell.Cell(
|
||||||
cell,
|
cell,
|
||||||
font, self.cell_hpos[cell_i],
|
font, hpos,
|
||||||
self.s_hline.has(row_i, cell_i),
|
self.s_hline.has(row_i, cell_i),
|
||||||
self.s_vline.has(right_i, row_i),
|
self.s_vline.has(right_i, row_i),
|
||||||
self.s_hline.has(bottom_i, cell_i),
|
self.s_hline.has(bottom_i, cell_i),
|
||||||
@@ -252,7 +488,8 @@ class Table(klammer_base.Klammer_base):
|
|||||||
self.s_vline.by_index.get(cell_i),
|
self.s_vline.by_index.get(cell_i),
|
||||||
self.s_vline.by_index.get(right_i),
|
self.s_vline.by_index.get(right_i),
|
||||||
rspan, cspan,
|
rspan, cspan,
|
||||||
first_column=(cell_i == 0)))
|
first_column=(cell_i == 0),
|
||||||
|
hpos_forced=(row_i, cell_i) in self.hpos_map))
|
||||||
cells.append(row_cells)
|
cells.append(row_cells)
|
||||||
self.cells = cells
|
self.cells = cells
|
||||||
self.column_width_text()
|
self.column_width_text()
|
||||||
|
|||||||
@@ -12,10 +12,13 @@ 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, first_column=False):
|
rowspan, colspan, first_column=False, hpos_forced=False):
|
||||||
self.text = text
|
self.text = text
|
||||||
self.font = font
|
self.font = font
|
||||||
self.hpos = hpos
|
self.hpos = hpos
|
||||||
|
# An :hpos override on an ordinary cell must reach the tex target
|
||||||
|
# through a \multicolumn{1} (the column spec sets the default).
|
||||||
|
self.hpos_forced = hpos_forced
|
||||||
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
|
||||||
@@ -74,7 +77,7 @@ class Cell:
|
|||||||
left_marker = "xL" if remove_left else "L" if self.border.left_all else ""
|
left_marker = "xL" if remove_left else "L" if self.border.left_all else ""
|
||||||
right_marker = "Rx" if remove_right else "R" if self.border.right_all else ""
|
right_marker = "Rx" if remove_right else "R" if self.border.right_all else ""
|
||||||
result = f"{left_marker} {result} {right_marker}"
|
result = f"{left_marker} {result} {right_marker}"
|
||||||
if remove_left or remove_right:
|
if remove_left or remove_right or self.hpos_forced:
|
||||||
pos = self.hpos
|
pos = self.hpos
|
||||||
if self.border.left_all and self.border.left:
|
if self.border.left_all and self.border.left:
|
||||||
pos = "|" + pos
|
pos = "|" + pos
|
||||||
|
|||||||
@@ -192,7 +192,10 @@ namespace latex {
|
|||||||
ss << "\\newcommand{\\documenttwocolumn}{" << (two_column ? "true" : "false") << "}\n";
|
ss << "\\newcommand{\\documenttwocolumn}{" << (two_column ? "true" : "false") << "}\n";
|
||||||
ss << "\\newcommand{\\documentlandscape}{" << (landscape ? "true" : "false") << "}\n";
|
ss << "\\newcommand{\\documentlandscape}{" << (landscape ? "true" : "false") << "}\n";
|
||||||
ss << "\\newcommand{\\documentisbook}{" << (book_format ? "true" : "false") << "}\n";
|
ss << "\\newcommand{\\documentisbook}{" << (book_format ? "true" : "false") << "}\n";
|
||||||
if (!bottom.empty()) {
|
// ":bottom none" suppresses the footer entirely: no \documentbottom,
|
||||||
|
// and \pagestyle{empty} replaces the sks page styles below.
|
||||||
|
bool no_footer = (bottom == "none");
|
||||||
|
if (!bottom.empty() && !no_footer) {
|
||||||
ss << "\\newcommand{\\documentbottom}{" << bottom << "}\n";
|
ss << "\\newcommand{\\documentbottom}{" << bottom << "}\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,7 +258,7 @@ namespace latex {
|
|||||||
ss << book_verso_page(copyright);
|
ss << book_verso_page(copyright);
|
||||||
|
|
||||||
// Table of contents (starts on page iii, recto)
|
// Table of contents (starts on page iii, recto)
|
||||||
ss << "\\pagestyle{noheader}\n"
|
ss << "\\pagestyle{" << (no_footer ? "empty" : "noheader") << "}\n"
|
||||||
<< "\\tableofcontents\n";
|
<< "\\tableofcontents\n";
|
||||||
|
|
||||||
// Advance to the next recto page for the body.
|
// Advance to the next recto page for the body.
|
||||||
@@ -270,9 +273,13 @@ namespace latex {
|
|||||||
ss << "\\clearpage\n"
|
ss << "\\clearpage\n"
|
||||||
<< "\\thispagestyle{empty}\\mbox{}\\clearpage\n"
|
<< "\\thispagestyle{empty}\\mbox{}\\clearpage\n"
|
||||||
<< "\\pagenumbering{arabic}\n"
|
<< "\\pagenumbering{arabic}\n"
|
||||||
<< "\\pagestyle{sksbook}\n";
|
<< "\\pagestyle{" << (no_footer ? "empty" : "sksbook") << "}\n";
|
||||||
|
} else {
|
||||||
|
if (no_footer) {
|
||||||
|
ss << "\\pagestyle{empty}\n\n";
|
||||||
} else {
|
} else {
|
||||||
ss << "\\pagestyle{sks" << structure << "}\n\n";
|
ss << "\\pagestyle{sks" << structure << "}\n\n";
|
||||||
|
}
|
||||||
// Plain or article: title block with optional copyright footnote
|
// Plain or article: title block with optional copyright footnote
|
||||||
if (!copyright.empty()) {
|
if (!copyright.empty()) {
|
||||||
ss << "\\renewcommand{\\thefootnote}{}\n";
|
ss << "\\renewcommand{\\thefootnote}{}\n";
|
||||||
|
|||||||
Reference in New Issue
Block a user