Escaping, table layout, and document fixes
Quoted Klammertext specials (^@ ^| ^# ^^ ^: ^*) and ^'...'^ regions now survive re-processing (held as escape markers until final output); :after_apply phase functions receive and return raw target text. Tables: :hpos element position (center|left|right|<length>) replaces the unimplemented :center/:indent; the ranged cell override is renamed :justify; :column_width works in html (colgroup widths) and gains 'fill' -- the remaining width, capped at the column's widest entry, in both targets; a table wider than the text column warns on the console; table edges without an outer line set their text flush on the margins. @document: no empty title bar for untitled documents; @vfill fills to the bottom of the window in html (pure CSS); @vspace in plain text; new @dot klammer; monospace email links.
This commit is contained in:
@@ -114,7 +114,7 @@ void check_cpp_arguments(katom_list args, Locator loc)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
katom_list Eval::eval(katom_iter begin, katom_iter end)
|
std::string Eval::eval_command(katom_iter begin, katom_iter end)
|
||||||
{
|
{
|
||||||
(void)K::log(3, *begin, *(end - 1));
|
(void)K::log(3, *begin, *(end - 1));
|
||||||
//msg() << "in Eval::eval:\n" << ktype << kall << kindex << std::pair(begin, end) << "\n";
|
//msg() << "in Eval::eval:\n" << ktype << kall << kindex << std::pair(begin, end) << "\n";
|
||||||
@@ -150,6 +150,12 @@ katom_list Eval::eval(katom_iter begin, katom_iter end)
|
|||||||
Eval_cpp E_cpp(m_machine, begin->m_loc);
|
Eval_cpp E_cpp(m_machine, begin->m_loc);
|
||||||
eval_result = E_cpp.eval(libpath, funcname);
|
eval_result = E_cpp.eval(libpath, funcname);
|
||||||
}
|
}
|
||||||
|
return eval_result;
|
||||||
|
}
|
||||||
|
|
||||||
|
katom_list Eval::eval(katom_iter begin, katom_iter end)
|
||||||
|
{
|
||||||
|
std::string eval_result = eval_command(begin, end);
|
||||||
katom_list result {};
|
katom_list result {};
|
||||||
Machine M = m_machine;
|
Machine M = m_machine;
|
||||||
size_t before = M.m_katoms.size();
|
size_t before = M.m_katoms.size();
|
||||||
|
|||||||
@@ -25,6 +25,14 @@ public:
|
|||||||
std::vector<Katom> eval(
|
std::vector<Katom> eval(
|
||||||
std::vector<Katom>::iterator begin, std::vector<Katom>::iterator end);
|
std::vector<Katom>::iterator begin, std::vector<Katom>::iterator end);
|
||||||
|
|
||||||
|
// Dispatch the @eval command (python/shell/haskell/cpp) and return its
|
||||||
|
// raw string result, without re-reading it as Klammertext. Used by
|
||||||
|
// :after_apply phase functions, whose input and output are final target
|
||||||
|
// text -- re-katomizing it would misparse target characters (a "@" in
|
||||||
|
// justified txt output) as Klammertext syntax.
|
||||||
|
std::string eval_command(
|
||||||
|
std::vector<Katom>::iterator begin, std::vector<Katom>::iterator end);
|
||||||
|
|
||||||
Machine m_machine;
|
Machine m_machine;
|
||||||
Locator m_loc;
|
Locator m_loc;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,6 +29,16 @@ Eval_python::Eval_python(Machine& machine, Locator loc)
|
|||||||
m_globals = PyDict_New();
|
m_globals = PyDict_New();
|
||||||
m_locals = PyDict_New();
|
m_locals = PyDict_New();
|
||||||
PyDict_SetItemString(m_globals, "__builtins__", PyEval_GetBuiltins());
|
PyDict_SetItemString(m_globals, "__builtins__", PyEval_GetBuiltins());
|
||||||
|
// The machine's result text, so :after_apply phase functions can take it
|
||||||
|
// as an argument (the Python counterpart of a :cpp phase function reading
|
||||||
|
// machine.m_result). Set directly rather than through the state's
|
||||||
|
// python_code() because document text cannot be safely embedded in a
|
||||||
|
// quoted Python source string.
|
||||||
|
PyObject* result_text = PyUnicode_FromString(m_machine.m_result.c_str());
|
||||||
|
if (result_text) {
|
||||||
|
PyDict_SetItemString(m_globals, "K_result", result_text);
|
||||||
|
Py_DECREF(result_text);
|
||||||
|
}
|
||||||
import_module("inspect", false);
|
import_module("inspect", false);
|
||||||
if (!m_machine.m_state.m_frames.empty()) {
|
if (!m_machine.m_state.m_frames.empty()) {
|
||||||
PyRun_String(m_machine.m_state.python_code().c_str(), Py_file_input, m_globals, m_locals);
|
PyRun_String(m_machine.m_state.python_code().c_str(), Py_file_input, m_globals, m_locals);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include "log.h"
|
#include "log.h"
|
||||||
#include "show.h"
|
#include "show.h"
|
||||||
#include "character.h"
|
#include "character.h"
|
||||||
|
#include "target.h"
|
||||||
|
|
||||||
std::string to_string(std::vector<Katom>::const_iterator begin, std::vector<Katom>::const_iterator end, bool trim_result)
|
std::string to_string(std::vector<Katom>::const_iterator begin, std::vector<Katom>::const_iterator end, bool trim_result)
|
||||||
{
|
{
|
||||||
@@ -299,7 +300,75 @@ void mark_literal_katoms(katom_list& katoms)
|
|||||||
//end->m_type = katom_t::replaced;
|
//end->m_type = katom_t::replaced;
|
||||||
mark_as_replaced(*(end - 1));
|
mark_as_replaced(*(end - 1));
|
||||||
//std::for_each(begin + 1, end, [](Katom& k) { k.m_type = katom_t::literal; });
|
//std::for_each(begin + 1, end, [](Katom& k) { k.m_type = katom_t::literal; });
|
||||||
std::for_each(begin + 1, end - 1, mark_as_literal);
|
// Hide Klammertext structural characters in the content as KTESC
|
||||||
|
// markers so the literal text survives re-katomization (the
|
||||||
|
// @document :text sub-Machine, the @eval result read-back).
|
||||||
|
// Resolved back to the characters at final processing. Literal
|
||||||
|
// KLAMMER content (@code) is NOT treated this way -- it is marked
|
||||||
|
// by mark_literal_klammer_content() and reaches the @eval code raw.
|
||||||
|
std::for_each(begin + 1, end - 1, [](Katom& k) {
|
||||||
|
mark_as_literal(k);
|
||||||
|
k.m_text = hide_structural_characters(k.m_text);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void hide_special_katoms(katom_list& katoms)
|
||||||
|
{
|
||||||
|
// Replace the text of ^-quoted special-character katoms (^@, ^|, ^#, ^^,
|
||||||
|
// ^:, ^*) with KTESC markers. The katomizer strips the "^" when the
|
||||||
|
// katom is constructed, so without this the bare character leaks into
|
||||||
|
// assembled strings (state values, @eval results) and is re-interpreted
|
||||||
|
// as Klammertext syntax when those strings are re-katomized -- by
|
||||||
|
// @document's :text sub-Machine or the @eval result read-back in
|
||||||
|
// Eval::eval. Markers are inert text at every level and are resolved to
|
||||||
|
// the characters at final processing (Target::resolve_escapes).
|
||||||
|
//
|
||||||
|
// Skipped inside:
|
||||||
|
// * @@...@@ and @@@...@@@ definition spans -- parameter declarations,
|
||||||
|
// descriptions, and argtype patterns are extracted as plain strings
|
||||||
|
// (kdesc display, validation regexes); a klammer BODY is re-processed
|
||||||
|
// through process_katoms() at application time, outside any
|
||||||
|
// definition span, so its quoted specials are hidden then.
|
||||||
|
// * @eval/@read/@cond argument spans -- code, filenames, and
|
||||||
|
// predicates consumed by the primitive, not target text (the same
|
||||||
|
// rule as the general-body escape pass in Machine::apply_klammer).
|
||||||
|
(void)K::log(4);
|
||||||
|
int definition_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 : katoms) {
|
||||||
|
switch (k.m_type) {
|
||||||
|
case katom_t::define_begin:
|
||||||
|
case katom_t::machine_begin:
|
||||||
|
++definition_depth;
|
||||||
|
continue;
|
||||||
|
case katom_t::define_end:
|
||||||
|
case katom_t::machine_end:
|
||||||
|
if (definition_depth > 0) --definition_depth;
|
||||||
|
continue;
|
||||||
|
case katom_t::eval_begin:
|
||||||
|
case katom_t::read_begin:
|
||||||
|
case katom_t::cond_begin:
|
||||||
|
apply_is_code.push_back(true);
|
||||||
|
++code_depth;
|
||||||
|
continue;
|
||||||
|
case katom_t::apply_begin:
|
||||||
|
apply_is_code.push_back(false);
|
||||||
|
continue;
|
||||||
|
case katom_t::apply_end:
|
||||||
|
if (!apply_is_code.empty()) {
|
||||||
|
if (apply_is_code.back()) --code_depth;
|
||||||
|
apply_is_code.pop_back();
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (k.m_type == katom_t::special &&
|
||||||
|
definition_depth == 0 && code_depth == 0) {
|
||||||
|
k.m_text = hide_structural_characters(k.m_text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ find_span_katoms(
|
|||||||
|
|
||||||
void encode_nonascii_characters(std::vector<Katom>& katoms);
|
void encode_nonascii_characters(std::vector<Katom>& katoms);
|
||||||
void mark_literal_katoms(std::vector<Katom>& katoms);
|
void mark_literal_katoms(std::vector<Katom>& katoms);
|
||||||
|
void hide_special_katoms(std::vector<Katom>& katoms);
|
||||||
void mark_ignored_katoms(std::vector<Katom>& katoms);
|
void mark_ignored_katoms(std::vector<Katom>& katoms);
|
||||||
|
|
||||||
void process_klammer_katoms(std::vector<Katom>& katoms);
|
void process_klammer_katoms(std::vector<Katom>& katoms);
|
||||||
|
|||||||
@@ -239,6 +239,7 @@ void Machine::process_katoms(
|
|||||||
{
|
{
|
||||||
mark_literal_klammer_content(katoms);
|
mark_literal_klammer_content(katoms);
|
||||||
if (literal) mark_literal_katoms(katoms);
|
if (literal) mark_literal_katoms(katoms);
|
||||||
|
hide_special_katoms(katoms);
|
||||||
if (nonascii) encode_nonascii_characters(katoms);
|
if (nonascii) encode_nonascii_characters(katoms);
|
||||||
if (ignore) mark_ignored_katoms(katoms);
|
if (ignore) mark_ignored_katoms(katoms);
|
||||||
if (whitespace) process_whitespace_modifiers(katoms);
|
if (whitespace) process_whitespace_modifiers(katoms);
|
||||||
@@ -516,13 +517,22 @@ std::string Machine::run_phase_functions()
|
|||||||
Target target = m_targets.get(m_state.value("K_target"), Locator());
|
Target target = m_targets.get(m_state.value("K_target"), Locator());
|
||||||
if (!target.m_after_apply.empty()) {
|
if (!target.m_after_apply.empty()) {
|
||||||
(void)K::log(2, target);
|
(void)K::log(2, target);
|
||||||
Eval E(*this, Locator());
|
|
||||||
for (auto f : target.m_after_apply) {
|
for (auto f : target.m_after_apply) {
|
||||||
|
// A mode-tagged spec (":cpp ...") names a function that receives
|
||||||
|
// the Machine itself; a bare Python function is called with the
|
||||||
|
// result text. The Eval is constructed per phase so a chained
|
||||||
|
// phase sees its predecessor's result in K_result.
|
||||||
|
Eval E(*this, Locator());
|
||||||
|
if (!f.empty() && f[0] != ':') {
|
||||||
|
f += "(K_result)";
|
||||||
|
}
|
||||||
f = "@eval " + f + " @";
|
f = "@eval " + f + " @";
|
||||||
auto katoms = katomize(line_split(f), "phase");
|
auto katoms = katomize(line_split(f), "phase");
|
||||||
katom_list eval_katoms = E.eval(katoms.begin(), katoms.end() - 2);
|
// A phase function's input and output are final target text, not
|
||||||
// msg() << "eval_katoms: " << eval_katoms << "\n";
|
// Klammertext: take the raw result string. Re-reading it as
|
||||||
m_result = to_string(eval_katoms.begin(), eval_katoms.end());
|
// Klammertext (Eval::eval) would misparse target characters --
|
||||||
|
// e.g. a "@" from a quoted ^@ in justified txt output.
|
||||||
|
m_result = E.eval_command(katoms.begin(), katoms.end() - 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return m_result;
|
return m_result;
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
#include "target.h"
|
#include "target.h"
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
#include "show.h"
|
#include "show.h"
|
||||||
@@ -85,20 +88,60 @@ std::string Target::escape_text(std::string text) const
|
|||||||
std::string Target::unescape_text(std::string text) const
|
std::string Target::unescape_text(std::string text) const
|
||||||
{
|
{
|
||||||
// Restore KTESC markers to original characters (for programmatic use)
|
// Restore KTESC markers to original characters (for programmatic use)
|
||||||
for (const auto& [ch, repl] : m_escapes) {
|
return ktesc_resolve(text);
|
||||||
text = string_replace(text, escape_marker(ch), ch);
|
|
||||||
}
|
|
||||||
return text;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Target::resolve_escapes(std::string text) const
|
std::string Target::resolve_escapes(std::string text) const
|
||||||
{
|
{
|
||||||
|
// Target-declared escapes first (marker -> declared replacement), then
|
||||||
|
// the generic decode for the remaining markers (marker -> the character
|
||||||
|
// itself: quoted Klammertext specials and literal-span content).
|
||||||
for (const auto& [ch, repl] : m_escapes) {
|
for (const auto& [ch, repl] : m_escapes) {
|
||||||
text = string_replace(text, escape_marker(ch), repl);
|
text = string_replace(text, escape_marker(ch), repl);
|
||||||
}
|
}
|
||||||
|
return ktesc_resolve(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ktesc_resolve(std::string text)
|
||||||
|
{
|
||||||
|
// Hand-rolled scan: no std::regex here, this runs over document-sized
|
||||||
|
// strings.
|
||||||
|
static const std::string tag = "KTESC";
|
||||||
|
size_t pos = 0;
|
||||||
|
while ((pos = text.find(tag, pos)) != std::string::npos) {
|
||||||
|
size_t start = pos + tag.size();
|
||||||
|
size_t close = text.find(tag, start);
|
||||||
|
if (close == std::string::npos) break;
|
||||||
|
size_t len = close - start;
|
||||||
|
bool is_hex = len > 0 && len % 4 == 0 &&
|
||||||
|
std::all_of(text.begin() + start, text.begin() + close,
|
||||||
|
[](unsigned char c) { return std::isxdigit(c) != 0; });
|
||||||
|
if (!is_hex) {
|
||||||
|
// Not a marker body; the closing tag may open a real marker.
|
||||||
|
pos = start;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
std::string chars {};
|
||||||
|
for (size_t i = start; i < close; i += 4)
|
||||||
|
chars += (char)std::stoi(text.substr(i, 4), nullptr, 16);
|
||||||
|
text.replace(pos, close + tag.size() - pos, chars);
|
||||||
|
pos += chars.size();
|
||||||
|
}
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string hide_structural_characters(const std::string& s)
|
||||||
|
{
|
||||||
|
std::string result {};
|
||||||
|
for (char c : s) {
|
||||||
|
if (c == '@' || c == '|' || c == '#' || c == '^' || c == ':' || c == '*')
|
||||||
|
result += Target::escape_marker(std::string(1, c));
|
||||||
|
else
|
||||||
|
result += c;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
void Target::add_after_apply(std::string function_specs)
|
void Target::add_after_apply(std::string function_specs)
|
||||||
{
|
{
|
||||||
for (auto f : regex_split(function_specs, std::regex(R"(\s+;\s+)"), true)) {
|
for (auto f : regex_split(function_specs, std::regex(R"(\s+;\s+)"), true)) {
|
||||||
|
|||||||
13
mac/target.h
13
mac/target.h
@@ -47,3 +47,16 @@ public:
|
|||||||
|
|
||||||
std::vector<std::pair<std::string, std::string>>
|
std::vector<std::pair<std::string, std::string>>
|
||||||
parse_transforms(std::string transform_string);
|
parse_transforms(std::string transform_string);
|
||||||
|
|
||||||
|
// Decode every KTESC<hex>KTESC marker in text back to its original
|
||||||
|
// characters. Used for the final output (after target-declared escapes have
|
||||||
|
// been resolved to their replacements) and for programmatic use of argument
|
||||||
|
// values. The Python counterpart is unescape_ktesc() in klammer_base.py.
|
||||||
|
std::string ktesc_resolve(std::string text);
|
||||||
|
|
||||||
|
// Replace each Klammertext structural character (@ | # ^ : *) in s with its
|
||||||
|
// KTESC marker, so text that has already been interpreted once (quoted
|
||||||
|
// specials, ^'...'^ literal content) survives re-katomization by
|
||||||
|
// sub-Machines and the @eval result read-back. Resolved by ktesc_resolve()
|
||||||
|
// at final processing.
|
||||||
|
std::string hide_structural_characters(const std::string& s);
|
||||||
|
|||||||
@@ -86,8 +86,23 @@ ANDY: QUOTE: *s*
|
|||||||
@@extendpage.tex :: \enlargethispage{*linecount*\baselineskip} @@
|
@@extendpage.tex :: \enlargethispage{*linecount*\baselineskip} @@
|
||||||
@@extendpage.txt :: @@
|
@@extendpage.txt :: @@
|
||||||
|
|
||||||
@@vspace.k length : Vertical space @@
|
@@vspace.k lines.float : Vertical space, in multiples of the current line height @@
|
||||||
@@vspace.tex :: \vspace*{*length*} @@
|
@@vspace.tex :: \vspace{*lines*\baselineskip} @@
|
||||||
|
@@vspace.html :: <div style="height: *lines*lh"></div> @@
|
||||||
|
@@vspace.txt :: @eval "__VSPACE__" * round(*lines*) @ @@
|
||||||
|
|
||||||
|
@@vfill.k :
|
||||||
|
Fill the vertical space so that any following text is flush with the bottom
|
||||||
|
of the page (in HTML, the bottom of the window; once the content is taller
|
||||||
|
than the window the space collapses, as on a full LaTeX page). Several
|
||||||
|
vfills divide the space equally, like LaTeX's \vfill glue. In plain text,
|
||||||
|
only makes some vertical space. @@
|
||||||
|
@@vfill.tex :: \vfill @@
|
||||||
|
# An empty glue div; block.css gives it flex-grow 1 and makes the text
|
||||||
|
# column a flex column only in documents that use it (the :has() rule).
|
||||||
|
@@vfill.html :: <div class="vfill"></div> @@
|
||||||
|
@@vfill.txt :: @vspace 3 @ @@
|
||||||
|
|
||||||
|
|
||||||
@@qa.k question | answer : Question and answer formatting @@
|
@@qa.k question | answer : Question and answer formatting @@
|
||||||
@@qa ::
|
@@qa ::
|
||||||
|
|||||||
@@ -24,3 +24,28 @@ p {
|
|||||||
.indent {
|
.indent {
|
||||||
margin-left: 2rem;
|
margin-left: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* @vfill: LaTeX's \vfill glue as flex-grow. The :has() rule turns the
|
||||||
|
text column into a flex column ONLY in documents that use @vfill (flex
|
||||||
|
containers do not collapse vertical margins, so paragraph spacing
|
||||||
|
shifts slightly there); several vfills share the free space equally,
|
||||||
|
like \vfill. The min-height ties the fill to the window: with a title
|
||||||
|
or status bar the column overshoots by their height (a small scroll);
|
||||||
|
content taller than the window collapses the glue, as on a full LaTeX
|
||||||
|
page. Fills the window, not a browser-printed page. */
|
||||||
|
#text:has(.vfill) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: calc(100vh - 1lh);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* One line of whitespace below the filled content, so the last block
|
||||||
|
does not touch the bottom of the window. A margin on the last flex
|
||||||
|
item, not #text padding: #middle's overflow clips the padding. */
|
||||||
|
#text:has(.vfill) > :last-child {
|
||||||
|
margin-bottom: 1lh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vfill {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,6 +26,46 @@ std::string document(Machine& machine)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Print a console warning for each KT-WIDE-TABLE marker a table's runtime
|
||||||
|
// width check (tex_width_check() in table.py) left in the xelatex log,
|
||||||
|
// followed by a short :column_width primer. Plain line scanning -- no
|
||||||
|
// std::regex over the (arbitrarily large) log text.
|
||||||
|
static void warn_wide_tables(const std::string& xelatex_log)
|
||||||
|
{
|
||||||
|
bool any = false;
|
||||||
|
std::istringstream lines(xelatex_log);
|
||||||
|
std::string line;
|
||||||
|
while (std::getline(lines, line)) {
|
||||||
|
auto pos = line.find("KT-WIDE-TABLE ");
|
||||||
|
if (pos == std::string::npos) continue;
|
||||||
|
if (!any) {
|
||||||
|
std::cerr << yellow
|
||||||
|
<< "Warning: a table is wider than the text column "
|
||||||
|
<< "and extends past the right margin:\n";
|
||||||
|
any = true;
|
||||||
|
}
|
||||||
|
std::string detail = line.substr(pos + 14);
|
||||||
|
// The log's newline encoding can leave a trailing backslash.
|
||||||
|
while (!detail.empty() &&
|
||||||
|
(detail.back() == '\\' || detail.back() == ' '))
|
||||||
|
detail.pop_back();
|
||||||
|
std::cerr << " " << detail << "\n";
|
||||||
|
}
|
||||||
|
if (any) {
|
||||||
|
std::cerr <<
|
||||||
|
" The :column_width values and how they interact:\n"
|
||||||
|
" fit the column's widest entry, never wrapped\n"
|
||||||
|
" fill the width left over after the other columns, but\n"
|
||||||
|
" no more than the widest entry; wraps when needed\n"
|
||||||
|
" 0.0-1.0 that fraction of the text column width\n"
|
||||||
|
" * the width left over, unconditionally (the table\n"
|
||||||
|
" always spans the full text column)\n"
|
||||||
|
" A long-text column set to \"fit\" never wraps and pushes\n"
|
||||||
|
" the table off the page; give it \"fill\" instead.\n"
|
||||||
|
<< black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extern "C"
|
extern "C"
|
||||||
std::string tex_to_pdf(Machine& machine)
|
std::string tex_to_pdf(Machine& machine)
|
||||||
{
|
{
|
||||||
@@ -67,6 +107,7 @@ std::string tex_to_pdf(Machine& machine)
|
|||||||
throw Definition_error(ss.str(), Locator(), false);
|
throw Definition_error(ss.str(), Locator(), false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
warn_wide_tables(xelatex_log);
|
||||||
/*
|
/*
|
||||||
if (std::stoi(machine.m_state.value("K_verbose_level")) < 2) {
|
if (std::stoi(machine.m_state.value("K_verbose_level")) < 2) {
|
||||||
for (auto ext : word_split("tex out aux log toc")) {
|
for (auto ext : word_split("tex out aux log toc")) {
|
||||||
|
|||||||
@@ -148,3 +148,8 @@ EB Garamond
|
|||||||
@@rightarrow.k : Right-pointing arrow: @rightarrow@ @@
|
@@rightarrow.k : Right-pointing arrow: @rightarrow@ @@
|
||||||
@@rightarrow.html :: &^#8594; @@
|
@@rightarrow.html :: &^#8594; @@
|
||||||
@@rightarrow.tex :: $\rightarrow$ @@
|
@@rightarrow.tex :: $\rightarrow$ @@
|
||||||
|
|
||||||
|
@@dot.k : Vertically entered dot @@
|
||||||
|
@@dot.tex :: $\cdot$ @@
|
||||||
|
@@dot.html :: ^00B7^ @@
|
||||||
|
@@dot.txt :: ^00B7^ @@
|
||||||
|
|||||||
@@ -18,8 +18,10 @@
|
|||||||
# @@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 |
|
@@@argtype image_hpos |
|
||||||
horizontal position of an image
|
horizontal position of an image: the element positions (center, left,
|
||||||
:pattern left^|center^|right^|none
|
right, or a length used as the left margin), or none for an inline image
|
||||||
|
with no positioning container
|
||||||
|
:pattern 'element_hpos'^|none
|
||||||
:default center
|
:default center
|
||||||
@@@
|
@@@
|
||||||
|
|
||||||
@@ -29,7 +31,6 @@
|
|||||||
:width.length .5w
|
:width.length .5w
|
||||||
@caption_arguments@
|
@caption_arguments@
|
||||||
:vmargin.bool true
|
:vmargin.bool true
|
||||||
:center.bool true
|
|
||||||
:hpos.image_hpos
|
:hpos.image_hpos
|
||||||
:rel
|
:rel
|
||||||
:abswidth.number 0.0
|
:abswidth.number 0.0
|
||||||
|
|||||||
@@ -35,6 +35,16 @@
|
|||||||
#:python_cast (lambda s : [__import__("kutil").parse_length("tex", e) for e in s.split()])
|
#:python_cast (lambda s : [__import__("kutil").parse_length("tex", e) for e in s.split()])
|
||||||
@@@
|
@@@
|
||||||
|
|
||||||
|
@@@argtype element_hpos |
|
||||||
|
the horizontal position of a block element (a table or an image) within
|
||||||
|
the text column: center, left, right, or a length, which places the
|
||||||
|
element's left edge that far from the left margin (e.g. ^:hpos 4em, ^:hpos
|
||||||
|
.25w). When the element is as wide as the text column, the positions are
|
||||||
|
indistinguishable.
|
||||||
|
:pattern center^|left^|right^|'length'
|
||||||
|
:default center
|
||||||
|
@@@
|
||||||
|
|
||||||
@@@argtype figure_id |
|
@@@argtype figure_id |
|
||||||
an identifier for a figure.
|
an identifier for a figure.
|
||||||
|
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ class Email(klammer_base.Klammer_base):
|
|||||||
if self.body:
|
if self.body:
|
||||||
href += val("body", self.body)
|
href += val("body", self.body)
|
||||||
href = href[:-1]
|
href = href[:-1]
|
||||||
result = '<a href="{}">{}</a>'.format(href, address)
|
result = '<a href="{}" class="monospace">{}</a>'.format(href, address)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def tex(self):
|
def tex(self):
|
||||||
|
|||||||
@@ -21,11 +21,18 @@ td {
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/* Edge cells of a table with no outer vertical line (classes emitted by
|
||||||
td:first-child {
|
table.py): the outer padding is dropped so the cell text aligns with
|
||||||
|
the text margin. With an outer line the padding stays -- text against
|
||||||
|
a border looks worse than text inset from a margin. The tex
|
||||||
|
counterpart is @{} in the column spec. */
|
||||||
|
.Fl {
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
.Fr {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
.line_top {
|
.line_top {
|
||||||
border-top: 1px black solid;
|
border-top: 1px black solid;
|
||||||
}
|
}
|
||||||
@@ -75,6 +82,21 @@ td:first-child {
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* A cell of a 'fit' column mixed with sized columns: nowrap floors the
|
||||||
|
column at its widest entry (the tex \widthof semantics). In a
|
||||||
|
full-width table (fractions/'*') the Wpct 1% width is added -- the
|
||||||
|
classic shrink idiom, so the column survives surplus distribution and
|
||||||
|
the extra window width flows to the sized columns. In a content-sized
|
||||||
|
'fill' table Wpct must NOT be used: a percentage cell blows an
|
||||||
|
auto-width table up to full width. */
|
||||||
|
.Wfit {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Wpct {
|
||||||
|
width: 1%;
|
||||||
|
}
|
||||||
|
|
||||||
.cell_arrow {
|
.cell_arrow {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
|
|||||||
@@ -28,3 +28,11 @@
|
|||||||
|
|
||||||
\newsavebox{\tablebox}
|
\newsavebox{\tablebox}
|
||||||
\newlength{\tableboxwidth}
|
\newlength{\tableboxwidth}
|
||||||
|
|
||||||
|
% Computed widths of :column_width 'fill' columns (up to four per table):
|
||||||
|
% min(share of the remaining width, widest entry), set per table in the
|
||||||
|
% generated LaTeX via calc's \minof/\ratio.
|
||||||
|
\newlength{\klfilla}
|
||||||
|
\newlength{\klfillb}
|
||||||
|
\newlength{\klfillc}
|
||||||
|
\newlength{\klfilld}
|
||||||
|
|||||||
@@ -30,11 +30,16 @@
|
|||||||
|
|
||||||
@@@argtype column_width |
|
@@@argtype column_width |
|
||||||
width of the table columns. Each column is one of 'fit' (widest line of the
|
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
|
cells in that column), 'fill' (the remaining width of the table after the
|
||||||
width), or '*' (use the remaining width of the table; there can only be one
|
other columns, but no more than the column's widest line -- the table stops
|
||||||
column with '*'). If there are fewer positions than columns in the table, the
|
growing once nothing needs a line break), a fraction 0.0->1.0 (that
|
||||||
last value is repeated. Extra positions generate a warning.
|
fraction of the total table width), or '*' (the remaining width,
|
||||||
:pattern (fit^|f^|0?\.\d+^|\*^|\s+)+
|
unconditionally -- the table always spans the full width). Several 'fill'
|
||||||
|
columns divide the remaining width in proportion to their widest lines;
|
||||||
|
'fill' cannot be combined with a fraction or '*'. If there are fewer
|
||||||
|
positions than columns in the table, the last value is repeated. Extra
|
||||||
|
positions generate a warning.
|
||||||
|
:pattern (fill^|fit^|f^|0?\.\d+^|\*^|\s+)+
|
||||||
:python_cast (lambda s : s.split())
|
:python_cast (lambda s : s.split())
|
||||||
:default fit
|
:default fit
|
||||||
@@@
|
@@@
|
||||||
@@ -154,15 +159,15 @@
|
|||||||
:default period
|
:default period
|
||||||
@@@
|
@@@
|
||||||
|
|
||||||
@@@argtype table_hpos |
|
@@@argtype table_justify |
|
||||||
cell position overrides, as one or more <cells> <position> pairs
|
cell justification overrides, as one or more <cells> <position> pairs
|
||||||
separated by semicolons (the same list style as ^:calc). <cells> is an
|
separated by semicolons (the same list style as ^:calc). <cells> is an
|
||||||
indexed_range selecting cells; <position> is l, c, or r and overrides
|
indexed_range selecting cells; <position> is l, c, or r and overrides
|
||||||
the column position given by ^:cell_hpos for those cells. A colspan
|
the column justification given by ^:cell_hpos for those cells. A colspan
|
||||||
anchor's override positions the whole merged cell. For example,
|
anchor's override justifies the whole merged cell. For example,
|
||||||
"-3--1(3) r" right-justifies the cells in column 3 of the last three
|
"-3--1(3) r" right-justifies the cells in column 3 of the last three
|
||||||
rows.
|
rows.
|
||||||
# Coarse check ("<cells> <position>" pairs); hpos_overrides() in
|
# Coarse check ("<cells> <position>" pairs); justify_overrides() in
|
||||||
# table.py validates the range and position.
|
# table.py validates the range and position.
|
||||||
:pattern \s*([^^\s;]+\s+[lcr]\s*(;\s*^|\s*$))+
|
:pattern \s*([^^\s;]+\s+[lcr]\s*(;\s*^|\s*$))+
|
||||||
@@@
|
@@@
|
||||||
@@ -188,8 +193,7 @@
|
|||||||
@@table rows.rest(2)
|
@@table rows.rest(2)
|
||||||
:id
|
:id
|
||||||
@caption_arguments@
|
@caption_arguments@
|
||||||
:center.bool true
|
:hpos.element_hpos
|
||||||
:indent.length 1em
|
|
||||||
:header.bool true
|
:header.bool true
|
||||||
:allow_break.bool false
|
:allow_break.bool false
|
||||||
:column_width.column_width
|
:column_width.column_width
|
||||||
@@ -197,7 +201,7 @@
|
|||||||
:vline.table_vline
|
:vline.table_vline
|
||||||
:grid.bool false
|
:grid.bool false
|
||||||
:cell_hpos.cell_hpos
|
:cell_hpos.cell_hpos
|
||||||
:hpos.table_hpos
|
:justify.table_justify
|
||||||
:header_font.font i
|
:header_font.font i
|
||||||
:font.font_list
|
:font.font_list
|
||||||
:colspan.table_span
|
:colspan.table_span
|
||||||
|
|||||||
@@ -85,7 +85,38 @@ class Table(klammer_base.Klammer_base):
|
|||||||
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 {}
|
# In html, a 'fit' column mixed with sized columns must be clamped
|
||||||
|
# to its widest entry (tex's \widthof semantics) or it absorbs the
|
||||||
|
# window width: its cells get nowrap (class Wfit; see table.css).
|
||||||
|
# In a full-width table (fractions/'*') they also need the classic
|
||||||
|
# 1%-shrink width (class Wpct) to survive surplus distribution; in
|
||||||
|
# a 'fill' table the table is content-sized (max-width) and a
|
||||||
|
# percentage would blow it up to full width, so nowrap alone.
|
||||||
|
# All-'fit' tables shrink to content anyway and keep wrapping.
|
||||||
|
widths = extend(self.column_width, self.row_size)
|
||||||
|
self.fill_columns = [i for i, w in enumerate(widths) if w == "fill"]
|
||||||
|
if self.fill_columns:
|
||||||
|
if any(w not in ("fit", "f", "fill") for w in widths):
|
||||||
|
raise Exception(
|
||||||
|
':column_width: "fill" cannot be combined with a '
|
||||||
|
'fraction or "*" -- fill computes the remaining width '
|
||||||
|
'itself')
|
||||||
|
if len(self.fill_columns) > 4:
|
||||||
|
raise Exception(
|
||||||
|
':column_width: at most four "fill" columns are '
|
||||||
|
'supported')
|
||||||
|
mixed = not all(w in ("fit", "f") for w in widths)
|
||||||
|
self.fit_columns = {i for i, w in enumerate(widths)
|
||||||
|
if mixed and w in ("fit", "f")}
|
||||||
|
self.fit_class = "Wfit" if self.fill_columns else "Wfit Wpct"
|
||||||
|
# A table edge with no outer vertical line drops its outer cell
|
||||||
|
# padding (html Fl/Fr classes, tex @{}) so the edge cells' text
|
||||||
|
# aligns with the text margin; with an outer line the padding
|
||||||
|
# stays -- text against a border looks worse than text inset
|
||||||
|
# from a margin.
|
||||||
|
self.flush_l = 0 not in self.s_vline.by_index
|
||||||
|
self.flush_r = self.row_size not in self.s_vline.by_index
|
||||||
|
self.justify_map = self.justify_overrides() if self.justify else {}
|
||||||
self.make_cells(self.rows)
|
self.make_cells(self.rows)
|
||||||
|
|
||||||
# Calculated cell values (:calc). A calculation is
|
# Calculated cell values (:calc). A calculation is
|
||||||
@@ -438,10 +469,10 @@ class Table(klammer_base.Klammer_base):
|
|||||||
# colspan anchor's override positions the whole merged cell; in the tex
|
# colspan anchor's override positions the whole merged cell; in the tex
|
||||||
# target an ordinary overridden cell is wrapped in \multicolumn{1}.
|
# target an ordinary overridden cell is wrapped in \multicolumn{1}.
|
||||||
|
|
||||||
def hpos_overrides(self):
|
def justify_overrides(self):
|
||||||
result = {}
|
result = {}
|
||||||
for stmt in [s.strip() for s in self.hpos.split(";") if s.strip()]:
|
for stmt in [s.strip() for s in self.justify.split(";") if s.strip()]:
|
||||||
ctx = f'In :hpos "{stmt}"'
|
ctx = f'In :justify "{stmt}"'
|
||||||
parts = stmt.split()
|
parts = stmt.split()
|
||||||
if len(parts) != 2 or parts[1] not in ("l", "c", "r"):
|
if len(parts) != 2 or parts[1] not in ("l", "c", "r"):
|
||||||
self.selector_error(
|
self.selector_error(
|
||||||
@@ -476,7 +507,7 @@ 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])
|
hpos = self.justify_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,
|
||||||
@@ -489,13 +520,54 @@ class Table(klammer_base.Klammer_base):
|
|||||||
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))
|
hpos_forced=(row_i, cell_i) in self.justify_map,
|
||||||
|
fit_class=(self.fit_class
|
||||||
|
if cell_i in self.fit_columns else ""),
|
||||||
|
flush_left=(self.flush_l and cell_i == 0),
|
||||||
|
flush_right=(self.flush_r and
|
||||||
|
cell_i + max(cspan, 1) == self.row_size)))
|
||||||
cells.append(row_cells)
|
cells.append(row_cells)
|
||||||
self.cells = cells
|
self.cells = cells
|
||||||
self.column_width_text()
|
self.column_width_text()
|
||||||
|
|
||||||
# HTML
|
# HTML
|
||||||
|
|
||||||
|
def html_colgroup(self):
|
||||||
|
# The CSS counterpart of tex_hpos()'s width resolution: 'fit' is the
|
||||||
|
# widest entry of the column and no larger (tex's \widthof — the
|
||||||
|
# cells' Wfit class clamps it there), a
|
||||||
|
# fraction is of the text column, and '*' shares the width the sized
|
||||||
|
# columns leave over. All-'fit' (the default) needs no markup at
|
||||||
|
# all. With no 'fit' column the fixed layout makes the fractions
|
||||||
|
# exact (long content wraps, as LaTeX's p{} columns do); a 'fit'
|
||||||
|
# column forces the auto layout, where the fraction widths are
|
||||||
|
# honored approximately.
|
||||||
|
widths = extend(self.column_width, self.row_size)
|
||||||
|
if all(w in ("fit", "f") for w in widths):
|
||||||
|
return "", "", None
|
||||||
|
fractions = sum(float(w) for w in widths if w not in ("fit", "f", "*"))
|
||||||
|
if any(w in ("fit", "f", "*") for w in widths):
|
||||||
|
table_width = "100%"
|
||||||
|
else:
|
||||||
|
table_width = f"{min(fractions, 1) * 100:g}%"
|
||||||
|
cols = ""
|
||||||
|
for w in widths:
|
||||||
|
if w in ("fit", "f"):
|
||||||
|
# Clamped to the widest entry by the cells' Wfit class
|
||||||
|
# (width 1% + nowrap; see table.css) -- a px width on the
|
||||||
|
# <col> does NOT survive surplus distribution when no '*'
|
||||||
|
# column exists (seen in both Firefox and Chrome).
|
||||||
|
cols += E("col").str()
|
||||||
|
elif w == "*":
|
||||||
|
cols += E("col").str()
|
||||||
|
else:
|
||||||
|
share = float(w) if table_width == "100%" else float(w) / fractions
|
||||||
|
cols += E("col").attr("style", f"width: {share * 100:g}%").str()
|
||||||
|
layout = "" if any(w in ("fit", "f") for w in widths) else "table-layout: fixed; "
|
||||||
|
# No blank line before </colgroup>: @document's insert_missing_ids
|
||||||
|
# would wrap it as a stray <p> inside the table.
|
||||||
|
return E("colgroup").body(cols.strip(), newline=False).str(), layout, table_width
|
||||||
|
|
||||||
def html(self):
|
def html(self):
|
||||||
result = ""
|
result = ""
|
||||||
for row_i, row in enumerate(self.cells):
|
for row_i, row in enumerate(self.cells):
|
||||||
@@ -505,21 +577,86 @@ class Table(klammer_base.Klammer_base):
|
|||||||
continue
|
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()
|
||||||
|
if self.fill_columns:
|
||||||
|
# 'fill': the table sizes itself -- the browser's auto layout
|
||||||
|
# computes min(available, widest entries) natively, so the fill
|
||||||
|
# column grows only until nothing needs a line break. Several
|
||||||
|
# fill columns share in proportion to their content (the auto
|
||||||
|
# algorithm), matching the tex \ratio division. The max-width
|
||||||
|
# cap goes on the caption wrapper when there is one (the table's
|
||||||
|
# own percentage would be circular in a shrink-to-fit wrapper).
|
||||||
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,
|
||||||
side=self.caption_side, font_size=self.caption_font_size)
|
self.caption_font, hpos=self.hpos,
|
||||||
|
side=self.caption_side,
|
||||||
|
font_size=self.caption_font_size, max_width="100%")
|
||||||
else:
|
else:
|
||||||
result = result.str()
|
result.attr("style", "max-width: 100%")
|
||||||
|
result = html_util.hpos_container(result, self.hpos).str()
|
||||||
|
return result
|
||||||
|
colgroup, layout, table_width = self.html_colgroup()
|
||||||
|
result = E("table").body(colgroup + result)
|
||||||
|
if self.number or self.caption:
|
||||||
|
# The caption wrapper carries the width (a percentage on the
|
||||||
|
# shrink-to-fit wrapper itself would be circular) and the table
|
||||||
|
# fills it -- which also makes the caption track the table.
|
||||||
|
if table_width:
|
||||||
|
result.attr("style", f"{layout}width: 100%")
|
||||||
|
result = html_util.add_caption(
|
||||||
|
result, "Table", self.number, self.caption, self.caption_font,
|
||||||
|
hpos=self.hpos, side=self.caption_side,
|
||||||
|
font_size=self.caption_font_size, width=table_width)
|
||||||
|
else:
|
||||||
|
# An uncaptioned table still gets the position container, so
|
||||||
|
# html and tex agree on where the table sits.
|
||||||
|
if table_width:
|
||||||
|
result.attr("style", f"{layout}width: {table_width}")
|
||||||
|
result = html_util.hpos_container(result, self.hpos).str()
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# LaTeX
|
# LaTeX
|
||||||
|
|
||||||
|
# The length registers holding computed 'fill' column widths, declared
|
||||||
|
# in table.sty; one per fill column, in column order.
|
||||||
|
fill_registers = ["\\klfilla", "\\klfillb", "\\klfillc", "\\klfilld"]
|
||||||
|
|
||||||
|
def tex_fill_widths(self):
|
||||||
|
# Set each 'fill' column's register to min(its share of the
|
||||||
|
# remaining width, its widest entry) -- the same rule the html auto
|
||||||
|
# layout applies. The shares divide the remaining width in
|
||||||
|
# proportion to the columns' widest entries (calc's \ratio): either
|
||||||
|
# the space covers them all and every column caps at its widest
|
||||||
|
# entry, or no column caps and all the space is used -- no stranded
|
||||||
|
# whitespace, and no iterative redistribution.
|
||||||
|
if not self.fill_columns:
|
||||||
|
return ""
|
||||||
|
widths = extend(self.column_width, self.row_size)
|
||||||
|
fit = [f"\\widthof{{{self.column_widths[i]}}}"
|
||||||
|
for i, w in enumerate(widths) if w in ("fit", "f")]
|
||||||
|
remaining = "\\tablewidth" + "".join(f" - {e}" for e in fit)
|
||||||
|
widest = {i: f"\\widthof{{{self.column_widths[i]}}}"
|
||||||
|
for i in self.fill_columns}
|
||||||
|
total = " + ".join(widest[i] for i in self.fill_columns)
|
||||||
|
result = ""
|
||||||
|
for k, i in enumerate(self.fill_columns):
|
||||||
|
reg = Table.fill_registers[k]
|
||||||
|
if len(self.fill_columns) == 1:
|
||||||
|
result += (f"\\setlength{{{reg}}}"
|
||||||
|
f"{{\\minof{{{remaining}}}{{{widest[i]}}}}}\n")
|
||||||
|
else:
|
||||||
|
result += (f"\\setlength{{{reg}}}{{({remaining})"
|
||||||
|
f"*\\ratio{{{widest[i]}}}{{{total}}}}}\n")
|
||||||
|
result += (f"\\setlength{{{reg}}}"
|
||||||
|
f"{{\\minof{{{reg}}}{{{widest[i]}}}}}\n")
|
||||||
|
return result
|
||||||
|
|
||||||
def tex_hpos(self):
|
def tex_hpos(self):
|
||||||
# One column specification per column: the width comes from
|
# One column specification per column: the width comes from
|
||||||
# :column_width ('fit', a fraction of \tablewidth, or '*' for the
|
# :column_width ('fit', 'fill' via its precomputed register, a
|
||||||
# remaining width), the justification from :cell_hpos.
|
# 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",
|
||||||
@@ -527,9 +664,13 @@ class Table(klammer_base.Klammer_base):
|
|||||||
return f">{{\\{command}}}p{{{s}}}"
|
return f">{{\\{command}}}p{{{s}}}"
|
||||||
|
|
||||||
widths = []
|
widths = []
|
||||||
|
fill_ordinal = 0
|
||||||
for i, w in enumerate(extend(self.column_width, self.row_size)):
|
for i, w in enumerate(extend(self.column_width, self.row_size)):
|
||||||
if w in ("fit", "f"):
|
if w in ("fit", "f"):
|
||||||
widths.append(f"\\widthof{{{self.column_widths[i]}}}")
|
widths.append(f"\\widthof{{{self.column_widths[i]}}}")
|
||||||
|
elif w == "fill":
|
||||||
|
widths.append(Table.fill_registers[fill_ordinal])
|
||||||
|
fill_ordinal += 1
|
||||||
elif w == "*":
|
elif w == "*":
|
||||||
widths.append(None)
|
widths.append(None)
|
||||||
else:
|
else:
|
||||||
@@ -548,6 +689,12 @@ class Table(klammer_base.Klammer_base):
|
|||||||
parts = [""] * (self.row_size * 2 + 1)
|
parts = [""] * (self.row_size * 2 + 1)
|
||||||
for i in self.s_vline.by_index:
|
for i in self.s_vline.by_index:
|
||||||
parts[i * 2] = "|"
|
parts[i * 2] = "|"
|
||||||
|
# A lineless table edge drops its outer \tabcolsep (matched by the
|
||||||
|
# \tablewidth arithmetic in tex()).
|
||||||
|
if self.flush_l:
|
||||||
|
parts[0] = "@{}"
|
||||||
|
if self.flush_r:
|
||||||
|
parts[-1] = "@{}"
|
||||||
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
|
||||||
# print("tex_column_spec:", "".join(parts))
|
# print("tex_column_spec:", "".join(parts))
|
||||||
@@ -615,9 +762,45 @@ class Table(klammer_base.Klammer_base):
|
|||||||
result += "}} \\\\ \\endlastfoot\n"
|
result += "}} \\\\ \\endlastfoot\n"
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def tex_position(self):
|
||||||
|
# Position a PAGE-BREAKING table with longtable's own glue (it
|
||||||
|
# cannot be boxed). A boxed table (allow_break false) is positioned
|
||||||
|
# by its :hpos wrapper instead; its glue is left neutral (\fill on
|
||||||
|
# both sides collapses in the exactly-fitting box), because a fixed
|
||||||
|
# length would overflow the box. A length is the left margin.
|
||||||
|
if not self.allow_break:
|
||||||
|
left, right = "\\fill", "\\fill"
|
||||||
|
elif self.hpos == "center":
|
||||||
|
left, right = "\\fill", "\\fill"
|
||||||
|
elif self.hpos == "left":
|
||||||
|
left, right = "0pt", "\\fill"
|
||||||
|
elif self.hpos == "right":
|
||||||
|
left, right = "\\fill", "0pt"
|
||||||
|
else:
|
||||||
|
length, _, _ = kutil.parse_length("tex", self.hpos, 1)
|
||||||
|
left, right = length, "\\fill"
|
||||||
|
return (f"\\setlength{{\\LTleft}}{{{left}}}\n"
|
||||||
|
f"\\setlength{{\\LTright}}{{{right}}}\n")
|
||||||
|
|
||||||
|
def tex_width_check(self, name):
|
||||||
|
# Emit a marker into the xelatex log when the measured table is
|
||||||
|
# wider than the text column (2pt tolerance for exactly-full-width
|
||||||
|
# tables). tex_to_pdf() in document.cpp scans the log for the
|
||||||
|
# marker and prints the console warning with the :column_width
|
||||||
|
# primer -- the widths are only known at LaTeX run time, and prose
|
||||||
|
# kept out of TeX avoids the log's 79-column line wrapping.
|
||||||
|
return ("\\ifdim\\tableboxwidth>\\dimexpr\\textwidth+2pt\\relax\n"
|
||||||
|
f"\\message{{^^JKT-WIDE-TABLE {name} overfull by "
|
||||||
|
"\\the\\dimexpr\\tableboxwidth-\\textwidth\\relax^^J}\n"
|
||||||
|
"\\fi\n")
|
||||||
|
|
||||||
def tex(self):
|
def tex(self):
|
||||||
result = self.get_width()
|
name = f"Reference-Table-{Table.id}"
|
||||||
result += f"\\renewcommand*{{\\arraystretch}}{{{self.leading}}}\n"
|
Table.id += 1
|
||||||
|
# The measuring \savebox must stay OUTSIDE the \tableboxwidth
|
||||||
|
# minipage below: it computes the width the minipage consumes.
|
||||||
|
measure = self.get_width() + self.tex_width_check(name)
|
||||||
|
result = f"\\renewcommand*{{\\arraystretch}}{{{self.leading}}}\n"
|
||||||
if self.allow_break:
|
if self.allow_break:
|
||||||
result += "\\vspace*{12pt}\n"
|
result += "\\vspace*{12pt}\n"
|
||||||
result += "\\begin{longtable}{"
|
result += "\\begin{longtable}{"
|
||||||
@@ -629,23 +812,37 @@ class Table(klammer_base.Klammer_base):
|
|||||||
result += "\\end{longtable}\n"
|
result += "\\end{longtable}\n"
|
||||||
|
|
||||||
if not self.allow_break:
|
if not self.allow_break:
|
||||||
|
# Box the table at its measured width so the caption tracks it
|
||||||
|
# and the box can be positioned as one piece; the LT glue then
|
||||||
|
# has no room and positioning falls to the :hpos wrapper. (The
|
||||||
|
# page-breaking table cannot be boxed -- there the LT glue
|
||||||
|
# positions and make_caption's \multicolumn tracks.)
|
||||||
|
result = latex_util.minipage(
|
||||||
|
result, "\\tableboxwidth", vertical="t", center=False)
|
||||||
if self.number or self.caption:
|
if self.number or self.caption:
|
||||||
result = latex_util.add_caption(
|
result = latex_util.add_caption(
|
||||||
result, "Table", self.number, self.caption, "\\tablewidth",
|
result, "Table", self.number, self.caption, "\\tableboxwidth",
|
||||||
side=self.caption_side, font_symbol=self.caption_font,
|
hpos=self.hpos, side=self.caption_side,
|
||||||
|
font_symbol=self.caption_font,
|
||||||
font_size=self.caption_font_size)
|
font_size=self.caption_font_size)
|
||||||
else:
|
else:
|
||||||
result = latex_util.caption_wrapper(result, "center")
|
result = latex_util.caption_wrapper(result, self.hpos)
|
||||||
|
result = measure + result
|
||||||
|
|
||||||
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}"
|
||||||
# The wrapper (add_caption/caption_wrapper) owns all vertical space
|
# The wrapper (add_caption/caption_wrapper) owns all vertical space
|
||||||
# around the table; longtable's own glue is zeroed.
|
# around the table; longtable's own glue is zeroed.
|
||||||
result = (f"\\setlength{{\\tabcolsep}}{{{self.colsep}}}\n"
|
result = (f"\\setlength{{\\tabcolsep}}{{{self.colsep}}}\n"
|
||||||
"\\setlength{\\LTpre}{0pt}\n"
|
"\\setlength{\\LTpre}{0pt}\n"
|
||||||
"\\setlength{\\LTpost}{0pt}\n"
|
"\\setlength{\\LTpost}{0pt}\n"
|
||||||
f"\\setlength{{\\tablewidth}}{{\\textwidth - {2 * self.row_size}\\tabcolsep}}\n"
|
+ self.tex_position() +
|
||||||
|
# 2 \tabcolsep per column, minus the ones @{} removes at
|
||||||
|
# flush (lineless) edges.
|
||||||
|
f"\\setlength{{\\tablewidth}}{{\\textwidth - "
|
||||||
|
f"{2 * self.row_size - self.flush_l - self.flush_r}\\tabcolsep}}\n"
|
||||||
|
# Fill widths need \tablewidth and must precede the
|
||||||
|
# measuring \savebox, whose column spec reads them.
|
||||||
|
+ self.tex_fill_widths()
|
||||||
+ result)
|
+ result)
|
||||||
result = re.sub(r"\newline", r"\\\\", result)
|
result = re.sub(r"\newline", r"\\\\", result)
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ 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, hpos_forced=False):
|
rowspan, colspan, first_column=False, hpos_forced=False,
|
||||||
|
fit_class="", flush_left=False, flush_right=False):
|
||||||
self.text = text
|
self.text = text
|
||||||
self.font = font
|
self.font = font
|
||||||
self.hpos = hpos
|
self.hpos = hpos
|
||||||
@@ -23,6 +24,17 @@ class Cell:
|
|||||||
self.rowspan = rowspan
|
self.rowspan = rowspan
|
||||||
self.colspan = colspan
|
self.colspan = colspan
|
||||||
self.first_column = first_column
|
self.first_column = first_column
|
||||||
|
# html only: the class(es) clamping a 'fit' column's cell to its
|
||||||
|
# widest entry -- "Wfit Wpct" in a full-width table, "Wfit" in a
|
||||||
|
# content-sized ('fill') table, "" when not a fit column.
|
||||||
|
self.fit_class = fit_class
|
||||||
|
# Cell sits on a table edge with no outer vertical line: its outer
|
||||||
|
# padding (html) / \tabcolsep (tex, via @{}) is removed so the text
|
||||||
|
# aligns with the text margin. A tex \multicolumn replaces the
|
||||||
|
# whole preamble entry including the @{}, so edge cells must
|
||||||
|
# re-emit it in their own spec.
|
||||||
|
self.flush_left = flush_left
|
||||||
|
self.flush_right = flush_right
|
||||||
#print("Cell:", text, hpos)
|
#print("Cell:", text, hpos)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@@ -47,6 +59,12 @@ class Cell:
|
|||||||
if pred:
|
if pred:
|
||||||
result.cls(cls_name)
|
result.cls(cls_name)
|
||||||
result.cls(f"H{self.hpos}")
|
result.cls(f"H{self.hpos}")
|
||||||
|
if self.fit_class:
|
||||||
|
result.cls(self.fit_class)
|
||||||
|
if self.flush_left:
|
||||||
|
result.cls("Fl")
|
||||||
|
if self.flush_right:
|
||||||
|
result.cls("Fr")
|
||||||
return result.str()
|
return result.str()
|
||||||
|
|
||||||
def tex(self, debug=False): # , left_line, right_line):
|
def tex(self, debug=False): # , left_line, right_line):
|
||||||
@@ -70,6 +88,10 @@ class Cell:
|
|||||||
pos = "|" + pos
|
pos = "|" + pos
|
||||||
if self.border.right:
|
if self.border.right:
|
||||||
pos = pos + "|"
|
pos = pos + "|"
|
||||||
|
if self.flush_left:
|
||||||
|
pos = "@{}" + pos
|
||||||
|
if self.flush_right:
|
||||||
|
pos = pos + "@{}"
|
||||||
return f"\\multicolumn{{{self.colspan}}}{{{pos}}}{{{result}}}"
|
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
|
||||||
@@ -83,6 +105,10 @@ class Cell:
|
|||||||
pos = "|" + pos
|
pos = "|" + pos
|
||||||
if self.border.right_all and self.border.right:
|
if self.border.right_all and self.border.right:
|
||||||
pos = pos + "|"
|
pos = pos + "|"
|
||||||
|
if self.flush_left:
|
||||||
|
pos = "@{}" + pos
|
||||||
|
if self.flush_right:
|
||||||
|
pos = pos + "@{}"
|
||||||
result = f"\\multicolumn{{1}}{{{pos}}}{{{result}}}"
|
result = f"\\multicolumn{{1}}{{{pos}}}{{{result}}}"
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
@@ -160,6 +160,14 @@
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* A length :hpos; the margin-left comes as an inline style. */
|
||||||
|
.hpos_indent {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: left;
|
||||||
|
}
|
||||||
|
|
||||||
.hpos_margin {
|
.hpos_margin {
|
||||||
margin: 0 1em 0 1em;
|
margin: 0 1em 0 1em;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -309,7 +309,7 @@ namespace html {
|
|||||||
elts.push_back(elt("style", css));
|
elts.push_back(elt("style", css));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!title.empty()) {
|
if (!trim(title).empty()) {
|
||||||
elts.push_back(elt("title", trim(title)));
|
elts.push_back(elt("title", trim(title)));
|
||||||
}
|
}
|
||||||
HTML result = elt("head", elts);
|
HTML result = elt("head", elts);
|
||||||
@@ -486,12 +486,16 @@ namespace html {
|
|||||||
elements_t body {};
|
elements_t body {};
|
||||||
if (!title.empty()) {
|
if (!title.empty()) {
|
||||||
elements_t title_bar {};
|
elements_t title_bar {};
|
||||||
|
if (!trim(title).empty()) {
|
||||||
title_bar.push_back(elt("span", trim(title)).attr("id", "title_text"));
|
title_bar.push_back(elt("span", trim(title)).attr("id", "title_text"));
|
||||||
|
}
|
||||||
if (!logo.empty()) {
|
if (!logo.empty()) {
|
||||||
title_bar.push_back(elt("span", trim(logo)).attr("id", "logo"));
|
title_bar.push_back(elt("span", trim(logo)).attr("id", "logo"));
|
||||||
}
|
}
|
||||||
|
if (!title_bar.empty()) {
|
||||||
body.push_back(elt("div", title_bar).attr("id", "title"));
|
body.push_back(elt("div", title_bar).attr("id", "title"));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (!nav.empty()) {
|
if (!nav.empty()) {
|
||||||
body.push_back(navigation(max_level));
|
body.push_back(navigation(max_level));
|
||||||
}
|
}
|
||||||
@@ -538,13 +542,19 @@ namespace html {
|
|||||||
|
|
||||||
void add_title(elements_t& body, std::string title, std::string logo)
|
void add_title(elements_t& body, std::string title, std::string logo)
|
||||||
{
|
{
|
||||||
|
// No title bar at all when there is nothing to put in it (an
|
||||||
|
// untitled document); a logo alone still gets the bar.
|
||||||
elements_t title_bar {};
|
elements_t title_bar {};
|
||||||
|
if (!trim(title).empty()) {
|
||||||
title_bar.push_back(elt("span", trim(title)).attr("id", "title_text"));
|
title_bar.push_back(elt("span", trim(title)).attr("id", "title_text"));
|
||||||
|
}
|
||||||
if (!logo.empty()) {
|
if (!logo.empty()) {
|
||||||
title_bar.push_back(elt("span", trim(logo)).attr("id", "logo"));
|
title_bar.push_back(elt("span", trim(logo)).attr("id", "logo"));
|
||||||
}
|
}
|
||||||
|
if (!title_bar.empty()) {
|
||||||
body.push_back(elt("div", title_bar).attr("id", "title"));
|
body.push_back(elt("div", title_bar).attr("id", "title"));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void add_nav(elements_t& body, int max_level)
|
void add_nav(elements_t& body, int max_level)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -193,8 +193,27 @@ def element_tag(element):
|
|||||||
def font_class(font):
|
def font_class(font):
|
||||||
return {"r" : "", "i" : "ritalic", "t" : "monospace", "s" : "sanserif"}[font]
|
return {"r" : "", "i" : "ritalic", "t" : "monospace", "s" : "sanserif"}[font]
|
||||||
|
|
||||||
|
def hpos_container(element, hpos):
|
||||||
|
# Wrap element in its horizontal-position container. hpos is left,
|
||||||
|
# center, right, none, or a length, which becomes the left margin
|
||||||
|
# (the tex counterparts are the \LTleft glue for tables and the
|
||||||
|
# \hspace* in latex_util.caption_wrapper).
|
||||||
|
style = None
|
||||||
|
if hpos not in ("left", "center", "right", "none"):
|
||||||
|
length, _, _ = kutil.parse_length("html", hpos, 1)
|
||||||
|
style = f"margin-left: {length}"
|
||||||
|
hpos = "indent"
|
||||||
|
result = E("div").cls("hpos_" + hpos).body(element)
|
||||||
|
if style:
|
||||||
|
result.attr("style", style)
|
||||||
|
if hpos not in ("center", "indent"):
|
||||||
|
result.cls("hpos_margin")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def add_caption(element, caption_label, number, caption_text,
|
def add_caption(element, caption_label, number, caption_text,
|
||||||
font_symbol="i", hpos="center", side="bottom", as_string=True, font_size=.9):
|
font_symbol="i", hpos="center", side="bottom", as_string=True,
|
||||||
|
font_size=.9, width=None, max_width=None):
|
||||||
tag = element_tag(element)
|
tag = element_tag(element)
|
||||||
# Caption
|
# Caption
|
||||||
caption = ""
|
caption = ""
|
||||||
@@ -232,12 +251,19 @@ def add_caption(element, caption_label, number, caption_text,
|
|||||||
#print("-"*80)
|
#print("-"*80)
|
||||||
|
|
||||||
element = E("div").cls("caption_" + side).attr("data-label", caption_label).body(element)
|
element = E("div").cls("caption_" + side).attr("data-label", caption_label).body(element)
|
||||||
|
if width:
|
||||||
|
# The element's width (e.g. a table's :column_width total) lives
|
||||||
|
# on this wrapper: the element fills it, and the caption tracks
|
||||||
|
# the element.
|
||||||
|
element.attr("style", f"width: {width}")
|
||||||
|
elif max_width:
|
||||||
|
# A content-sized element (a 'fill' table): the wrapper shrinks
|
||||||
|
# to it but never past the text column, so the element wraps at
|
||||||
|
# narrow windows instead of overflowing.
|
||||||
|
element.attr("style", f"max-width: {max_width}")
|
||||||
|
|
||||||
|
|
||||||
result = element
|
result = hpos_container(element, hpos)
|
||||||
result = E("div").cls("hpos_" + hpos).body(element)
|
|
||||||
if hpos != "center":
|
|
||||||
result = result.cls("hpos_margin")
|
|
||||||
|
|
||||||
if number:
|
if number:
|
||||||
result.cls("element_container")
|
result.cls("element_container")
|
||||||
|
|||||||
@@ -55,15 +55,21 @@ def minipage(content, width="\\textwidth", vertical="c", center=True, vmargin=""
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def caption_wrapper(element, hpos, bottom_margin=.67):
|
def caption_wrapper(element, hpos, bottom_margin=.67):
|
||||||
|
# hpos is left, center, right, none (no wrapper), or a length, which
|
||||||
|
# becomes the left margin. The element is a box on a line inside a
|
||||||
|
# full-width minipage; \hfill on the empty side pushes it into place.
|
||||||
vmargin = f"{bottom_margin}\\baselineskip"
|
vmargin = f"{bottom_margin}\\baselineskip"
|
||||||
result = element
|
|
||||||
if hpos == "center":
|
if hpos == "center":
|
||||||
result = minipage(element, vmargin=vmargin)
|
return minipage(element, vmargin=vmargin)
|
||||||
elif hpos == "left":
|
if hpos == "left":
|
||||||
result = minipage("\\hfill" + element, vmargin=vmargin)
|
return minipage(element + "\\hfill", vmargin=vmargin, center=False)
|
||||||
elif hpos == "right":
|
if hpos == "right":
|
||||||
result = minipage(element + "\\hfill", vmargin=vmargin)
|
return minipage("\\hfill" + element, vmargin=vmargin, center=False)
|
||||||
return result
|
if hpos == "none":
|
||||||
|
return element
|
||||||
|
length, _, _ = kutil.parse_length("tex", hpos, 1)
|
||||||
|
return minipage(f"\\hspace*{{{length}}}" + element + "\\hfill",
|
||||||
|
vmargin=vmargin, center=False)
|
||||||
|
|
||||||
def make_caption_text(number, label, text, font_symbol, font_size):
|
def make_caption_text(number, label, text, font_symbol, font_size):
|
||||||
caption = None
|
caption = None
|
||||||
|
|||||||
@@ -304,17 +304,28 @@ def make_pdf_from_tex(filename, K, twice=True):
|
|||||||
os.system('rm -rf {}/{}.{}'.format(dirname, basename, unused_ext))
|
os.system('rm -rf {}/{}.{}'.format(dirname, basename, unused_ext))
|
||||||
|
|
||||||
|
|
||||||
|
# Emitted by @vspace.txt (sks/block/block.k), one marker per line of space.
|
||||||
|
# An @eval result consisting only of whitespace is trimmed away by the
|
||||||
|
# Klammermachine, so vertical space must travel as markers and become
|
||||||
|
# newlines here, after blank-line runs have been normalized.
|
||||||
|
vspace_marker = "__VSPACE__"
|
||||||
|
|
||||||
def justify_blocks(text, K=None):
|
def justify_blocks(text, K=None):
|
||||||
rgx = re.compile("\n\n+", re.S)
|
|
||||||
delim = '__DIVIDE__'
|
delim = '__DIVIDE__'
|
||||||
text = rgx.sub(delim, text)
|
text = re.sub("\n\n+", delim, text)
|
||||||
result = ""
|
result = ""
|
||||||
for par in text.split(delim):
|
for par in text.split(delim):
|
||||||
#print(par)
|
stripped = par.strip()
|
||||||
if par[0] not in {' ', '['}:
|
n = stripped.count(vspace_marker)
|
||||||
|
if n and stripped == vspace_marker * n:
|
||||||
|
# A paragraph of only @vspace markers: n blank lines in addition
|
||||||
|
# to the normal paragraph separation.
|
||||||
|
result += "\n" * n
|
||||||
|
continue
|
||||||
|
if par and par[0] not in {' ', '['}:
|
||||||
par = "\n".join(textwrap.wrap(par, width=80))
|
par = "\n".join(textwrap.wrap(par, width=80))
|
||||||
result += par + "\n\n"
|
result += par + "\n\n"
|
||||||
return result
|
return result.replace(vspace_marker, "\n")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ The LaTeX transformations supported by the SKS are:
|
|||||||
@@@
|
@@@
|
||||||
|
|
||||||
@@@target tex | LaTeX
|
@@@target tex | LaTeX
|
||||||
:escape \ \textbackslash{} & \& { \{ } \} $ \$ % \% _ \_
|
:escape \ \textbackslash{} & \& { \{ } \} $ \$ % \% _ \_ ^# \^# ^^ \textasciicircum{}
|
||||||
@@@
|
@@@
|
||||||
|
|
||||||
@@@target pdf | PDF from LaTeX
|
@@@target pdf | PDF from LaTeX
|
||||||
|
|||||||
Reference in New Issue
Block a user