txt section numbering, @typeface :middle/:lift and html scale, NewGardinerOmni, html :leading (from dev 9fa469dd74ed)

Section headings under txt are numbered inside a @document only; @typeface
gains :middle and :lift and its :scale now means the same in html and
pdf; NewGardinerOmni ships (OFL 1.1, Mark-Jan Nederhof) and @egyptian /
@chinese replace @cjk; @table :leading and :colsep reach html; kdesc
--typeface help names the flag; @document.txt is a Python class.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
ackandClaude Fable 5.1 committed 2026-09-18 20:02:44 +02:00
1 parent 0ead40f272
commit 63956c00e5
26 files changed
+436 -172

No files matched your search

+1 -1
View File
@@ -51,7 +51,7 @@ are regenerated on each release — patches cannot be merged directly.
Report problems (or send patches) to the author; accepted changes are Report problems (or send patches) to the author; accepted changes are
applied to the development tree and appear in a following snapshot. applied to the development tree and appear in a following snapshot.
This snapshot was assembled from development commit `d6993110e9b3`. This snapshot was assembled from development commit `9fa469dd74ed`.
## License ## License
+9 -9
View File
@@ -17,18 +17,18 @@ static void font_usage()
{ {
std::cout << std::cout <<
"Font maintenance commands:\n" "Font maintenance commands:\n"
" --font List the available fonts\n" " --typeface List the available fonts\n"
" --font list The same\n" " --typeface list The same\n"
" --font samples <output-dir> Write a sample page of the available fonts\n" " --typeface samples <output-dir> Write a sample page of the available fonts\n"
" to <output-dir>/index.html\n" " to <output-dir>/index.html\n"
" --font samples <input-dir> <output-dir>\n" " --typeface samples <input-dir> <output-dir>\n"
" Write a sample page for the (not yet\n" " Write a sample page for the (not yet\n"
" installed) font files under <input-dir>\n" " installed) font files under <input-dir>\n"
" --font install <input-dir> Install the font files found under\n" " --typeface install <input-dir> Install the font files found under\n"
" <input-dir> into the user font directory\n" " <input-dir> into the user font directory\n"
" --font install <input-dir> <output-dir>\n" " --typeface install <input-dir> <output-dir>\n"
" Install into <output-dir> instead\n" " Install into <output-dir> instead\n"
" --font help This description\n" " --typeface help This description\n"
"\n" "\n"
"Fonts are searched in the directories of the KLAMMERTEXT_FONTS\n" "Fonts are searched in the directories of the KLAMMERTEXT_FONTS\n"
"environment variable (colon-separated; default $HOME/.klammertext/fonts)\n" "environment variable (colon-separated; default $HOME/.klammertext/fonts)\n"
@@ -141,7 +141,7 @@ static void font_command(const strings_t& words)
} else if (verb == "help") { } else if (verb == "help") {
font_usage(); font_usage();
} else { } else {
std::cout << "Unrecognized font command: --font " << join(words, " ") std::cout << "Unrecognized typeface command: --typeface " << join(words, " ")
<< "\n\n"; << "\n\n";
font_usage(); font_usage();
} }
@@ -256,7 +256,7 @@ int main(int argc, char* argv[])
describe_rewrite_patterns(); describe_rewrite_patterns();
} }
// The --font subcommands operate on the Klammertext font store // The --typeface subcommands operate on the Klammertext font store
// (infrastructure) and load no klammer set. // (infrastructure) and load no klammer set.
if (args.given("typeface")) { if (args.given("typeface")) {
font_command(option_words(args, "typeface")); font_command(option_words(args, "typeface"));
+7
View File
@@ -0,0 +1,7 @@
@font-face {
font-family: 'NewGardinerOmni';
font-style: normal;
font-weight: 400;
src: url('newgardineromni/Regular.ttf') format('truetype');
}
+94
View File
@@ -0,0 +1,94 @@
Copyright (c) 2020, Mark-Jan Nederhof https://mjn.host.cs.st-andrews.ac.uk,
with Reserved Font Name NewGardiner.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
+9
View File
@@ -175,6 +175,15 @@ strings_t available_font_families()
return result; return result;
} }
bool font_installed(const std::string& name)
{
const std::string wanted = name_to_dirname(name);
for (const std::string& family : available_font_families())
if (name_to_dirname(family) == wanted)
return true;
return false;
}
static void extract_font_metrics(Resolved_font& font) static void extract_font_metrics(Resolved_font& font)
{ {
+6
View File
@@ -28,6 +28,12 @@ std::vector<std::string> installed_font_dirs();
std::string default_font_dir(); std::string default_font_dir();
// Every installable font family found across those directories. // Every installable font family found across those directories.
std::vector<std::string> available_font_families(); std::vector<std::string> available_font_families();
// Whether the store can resolve NAME -- written as the family name or as
// the store's directory name, since both resolve. The question a klammer
// asks before it names a typeface that is not shipped (a CJK face), so it
// can render the text plainly and say how to install the font instead of
// failing the document. Never throws; resolve_font() is the error path.
bool font_installed(const std::string& name);
// The Locator is the DOCUMENT location to blame when the typeface is not // The Locator is the DOCUMENT location to blame when the typeface is not
// installed: without it the "not installed" error cites font_store.cpp, // installed: without it the "not installed" error cites font_store.cpp,
// a source location no user can act on (the no-.cpp-location rule). // a source location no user can act on (the no-.cpp-location rule).
+52 -25
View File
@@ -4,6 +4,7 @@
#include "character.h" #include "character.h"
#include "error.h" #include "error.h"
#include "show.h" #include "show.h"
#include "font_store.h"
// extern "C" is needed for dlsym name lookup. Returning std::string from // extern "C" is needed for dlsym name lookup. Returning std::string from
// C-linkage functions is technically non-standard but works correctly on // C-linkage functions is technically non-standard but works correctly on
@@ -21,8 +22,11 @@ std::string diacritics_examples([[maybe_unused]] Machine& machine)
//std::cout << machine; //std::cout << machine;
std::stringstream kt {}; std::stringstream kt {};
kt << "@table :caption Diacritics (with typical base characters) :cell_hpos c c l |\n"
<< "@i-Displayed | @i-Written | @i-Name ||\n"; kt << "@s2 Roman letters with a diacritical mark @\n\n";
kt << "@table :caption Diacritics (with typical base characters) :cell_hpos c c l :leading 1.2 |\n"
<< "Displayed | Written | Name ||\n";
for (char symbol : diacritic_symbols_order) { for (char symbol : diacritic_symbols_order) {
std::string sym { symbol }; std::string sym { symbol };
@@ -43,8 +47,10 @@ extern "C"
std::string extended_latin_examples([[maybe_unused]] Machine& machine) std::string extended_latin_examples([[maybe_unused]] Machine& machine)
{ {
std::stringstream kt {}; std::stringstream kt {};
kt << "@table :caption Extended Latin characters and ligatures :cell_hpos c c l |\n" kt << "@s2 Non-Roman characters @\n\n";
<< "@i-Displayed | @i-Written | @i-Name ||\n";
kt << "@table :caption Extended Latin characters and ligatures :cell_hpos c c l :leading 1.2 |\n"
<< "Displayed | Written | Name ||\n";
for (const std::string& symbol : extended_latin_symbols) { for (const std::string& symbol : extended_latin_symbols) {
auto [code, name] = extended_latin[symbol]; auto [code, name] = extended_latin[symbol];
std::string coded = "^" + symbol + "^"; std::string coded = "^" + symbol + "^";
@@ -59,8 +65,10 @@ extern "C"
std::string pinyin_examples([[maybe_unused]] Machine& machine) std::string pinyin_examples([[maybe_unused]] Machine& machine)
{ {
std::stringstream kt {}; std::stringstream kt {};
kt << "@table :caption Mandarin pinyin tones (using vowel ``a'') :cell_hpos c c l |\n" kt << "@s2 Chinese Pinyin syllable accents @\n\n";
<< " @i-Displayed | @i-Written | @i-Name ||\n";
kt << "@table :caption Mandarin pinyin tones (using vowel ``a'') :cell_hpos c c l :leading 1.2 |\n"
<< "Displayed | Written | Name ||\n";
for (auto [symbol, codename] : pinyin_tones) { for (auto [symbol, codename] : pinyin_tones) {
auto [code, name] = codename; auto [code, name] = codename;
@@ -79,33 +87,24 @@ std::string unicode_examples([[maybe_unused]] Machine& machine)
<< "To specify a wide variety of different letter forms, the Unicode Standard " << "To specify a wide variety of different letter forms, the Unicode Standard "
<< "defines a large set of \"code points\" organized by groups. To include a " << "defines a large set of \"code points\" organized by groups. To include a "
<< "Unicode character in Klammertext, you surround its hexadecimal number with " << "Unicode character in Klammertext, you surround its hexadecimal number with "
<< "the \"^^\" character. For example, the Cyrillic captial letter \"I\" is \"И\", " << "the \"^^\" character. For example, the Cyrillic capital letter \"I\" is \"И\", "
<< "included in Klammertext as ^^0418^^.\n\n" << "included in Klammertext as @t ^^0418^^ @.\n\n"
<< "To search for Unicode characters by their description, see: @nl@\n" << "To search for Unicode characters by their description, see: "
<< "@link https://www.fileformat.info/info/unicode/char/search.htm @\n\n"; << "@link https://www.fileformat.info/info/unicode/char/search.htm @\n\n";
kt << "@table :caption Examples of Chinese characters :cell_hpos c c l |\n" kt << "@s3 Egyptian hieroglyphs @\n\n";
<< " Character | Written | Pinyin ||\n";
for (auto chr : dao) { kt << "@table :caption Examples of Egyptian hieroglyphs :cell_hpos c c c l :leading 1.1 |\n"
std::string code = "^" + chr[1] + "^";
kt << process_unicode_codepoint(code) << " | "
<< "^" << code << "^" << " | "
<< chr[0] << " ||\n";
}
kt << "table@\n\n";
kt << "@table :caption Examples of Egyptian hieroglyphs :cell_hpos c c c l |\n"
<< "Hieroglyph | Written | Sound | Gardiner ||\n"; << "Hieroglyph | Written | Sound | Gardiner ||\n";
// The body font carries no hieroglyphs, so the glyph names the // The body font carries no hieroglyphs, so the glyph names the
// typeface it needs (shipped in fnt/); @document declares and localizes // typeface it needs (shipped in fnt/); @document declares and localizes
// it. Under txt @typeface passes the text through unchanged. // it. Under txt @typeface passes the text through unchanged.
for (auto glyph : ptolemy) { for (auto glyph : ptolemy) {
kt << "@typeface noto-sans-egyptian-hieroglyphs :scale 1.5 | " std::string code = "^" + glyph[2] + "^";
<< process_unicode_codepoint("^" + glyph[2] + "^") << " @ | " kt << "@egyptian "
<< "^^" + glyph[2] + "^^ | " << process_unicode_codepoint(code) << " @ | "
<< "@t ^'" << code << "'^ @ | "
<< glyph[1] << " | " << glyph[1] << " | "
<< glyph[0] << " ||\n"; << glyph[0] << " ||\n";
} }
@@ -114,6 +113,34 @@ std::string unicode_examples([[maybe_unused]] Machine& machine)
<< "the Unicode codepoint in the second column of the \"List of hieroglyphs\" " << "the Unicode codepoint in the second column of the \"List of hieroglyphs\" "
<< "table is the number in the ^^...^^ code used by Klammertext.\n\n"; << "table is the number in the ^^...^^ code used by Klammertext.\n\n";
kt << "@s3 Chinese characters @\n\n";
bool chinese = font_installed("Noto Serif SC");
std::string target = machine.m_state.get("K_target").m_value;
const bool txt_or_ansi = target == "txt"|| target == "ansi";
if (txt_or_ansi || chinese) {
kt << "@table :caption Examples of Chinese characters :cell_hpos c c l :leading 1.2 |\n"
<< " Character | Written | Pinyin ||\n";
for (auto chr : dao) {
std::string code = "^" + chr[1] + "^";
kt << "@chinese " << process_unicode_codepoint(code) << " @ | "
<< "@t ^'" << code << "'^ @" << " | "
<< chr[0] << " ||\n";
}
kt << "table@\n\n";
}
if (!txt_or_ansi && !chinese) {
kt << "To display Chinese characters, you need to install a Chinese typeface, "
"which Klammertext does not ship. "
"Download @i Noto Serif SC @ from Google Fonts, unzip it, and run: \n\n"
" @t kdesc ^-^-typeface install @ ~ @i the-installed-font-directory @ \n\n"
"The font goes into your "
"@t ^~/.klammertext/fonts @ directory or the first directory in the list of "
"directories defined by the @t KLAMMERTEXT_FONTS @ environment variable.\n\n";
}
return kt.str(); return kt.str();
} }
@@ -136,7 +163,7 @@ std::string argtype_definitions(Machine& machine)
auto filename = argtype.m_loc.m_filename; auto filename = argtype.m_loc.m_filename;
if (contains(filename, "argtype_registry")) { if (contains(filename, "argtype_registry")) {
std::string fill(name_width - name.size(), '~'); std::string fill(name_width - name.size(), '~');
kt << "\n@c " << name << " c@ @t " << fill << " t@ @i Pattern: i@ @c " kt << "\n@c " << name << " c@ @t " << fill << " t@ @i Pattern: i@ ~ @c "
<< argtype.m_symbolic_pattern << " c@ |\n" << argtype.m_symbolic_pattern << " c@ |\n"
<< capitalize(trim(argtype.m_desc)) << " ||\n"; << capitalize(trim(argtype.m_desc)) << " ||\n";
} else { } else {
+4 -1
View File
@@ -51,7 +51,10 @@ document.so : $(LOCAL_OBJECTS) $(SKS_OBJECTS) $(LIBRARY) document.cpp | deps
document.cpp -o $@ $(ALL_OBJECTS) -lklammertext $(LDLIBS) document.cpp -o $@ $(ALL_OBJECTS) -lklammertext $(LDLIBS)
clean : clean :
rm -f $(LOCAL_OBJECTS) $(LOCAL_DEPFILES) *.so *~ __pycache__ rm -f $(LOCAL_OBJECTS) $(LOCAL_DEPFILES) *.so *~
# document_txt.py is imported at render time (2026-09-18), so this
# directory now exists here; rm -f refused it and failed every clean.
rm -rf __pycache__
# Include generated dependency files (if they exist) # Include generated dependency files (if they exist)
-include $(LOCAL_DEPFILES) -include $(LOCAL_DEPFILES)
+4 -15
View File
@@ -62,21 +62,10 @@
: Top-level document structure : Top-level document structure
@@ @@
@@document.html,tex :: @eval :cpp *KLAMMERTEXT_HOME*/sks/document/document document @ @@ @@document.html,tex ::
@eval :cpp *KLAMMERTEXT_HOME*/sks/document/document document @
# The following definition of @document.txt is not adequate (it ignores :files, @@
# for example), but it enables tests of other txt klammers for now. When one of
# the arguments is empty, the justifcation postprocess should remove multiple
# lines, but __VSPACE__ inserts a space character, which prevents the lines
# removal from the justification function. You can also see the extra space
# before the *subtitle* value.
@@document.txt :: @@document.txt ::
@eval "*title*".upper() @ @nl@ @eval document_txt.Document(K) @
*subtitle*
*author* @nl@
*date*
*text*
@@ @@
-23
View File
@@ -1,23 +0,0 @@
import kutil
import html_util
import tex_util
class Document(klammer_base.Klammer_base):
def __init__(self, K):
super().__init__(K)
def html(self):
if self.include_css == "true":
css_code, css_files = html_util.css(self.css, self._sks_enabled)
head = html_util.head(self.title, css_files=css_files, css_code=css_code)
else:
head = head = html_util.head(self.title, include_fonts=self.include_fonts == "true")
return html_util.page(head, self.body)
def tex(self):
ragged_right = self.ragged_right in {"true", "True", "1"}
return tex_util.page(
self.title, self.subtitle, self.leading, self.pointsize, ragged_right, self.body)
def txt(self):
return self.body
+6 -20
View File
@@ -340,9 +340,14 @@ std::string Document_class::typeface_preamble()
std::stringstream ss {}; std::stringstream ss {};
for (size_t i = 0; i < m_extra_typefaces.size(); i++) { for (size_t i = 0; i < m_extra_typefaces.size(); i++) {
const auto& [name, scale] = m_extra_typefaces[i]; const auto& [name, scale] = m_extra_typefaces[i];
// Compared by directory name, as collect_typefaces() compares: a
// @typeface may write the family name or the store's directory
// name, and family_name holds what the css declares (2026-09-12),
// so a name-for-name comparison missed the directory spelling and
// left \ktface<n> undefined in the .tex (found 2026-09-17).
const Resolved_font* rf = nullptr; const Resolved_font* rf = nullptr;
for (const auto& f : m_resolved_extra) for (const auto& f : m_resolved_extra)
if (f.family_name == name) rf = &f; if (f.dir_name == name_to_dirname(name)) rf = &f;
if (!rf) continue; if (!rf) continue;
// \newfontfamily declares a switch command, unlike \setmainfont, // \newfontfamily declares a switch command, unlike \setmainfont,
// which changes the document's font permanently. // which changes the document's font permanently.
@@ -351,22 +356,3 @@ std::string Document_class::typeface_preamble()
} }
return ss.str(); return ss.str();
} }
/*
void output_msg(const std::string& msg)
{
std::cout << red;
(void)K::log(1, msg);
std::cout << black;
}
void Document_class::create_directory(const std::string& directory)
{
if (write_files) {
fs::create_directories(directory);
} else {
output_msg("Output directory: " + directory);
}
}
*/
+19
View File
@@ -1,3 +1,4 @@
#include <iomanip>
#include "util.h" #include "util.h"
#include "error.h" #include "error.h"
#include "log.h" #include "log.h"
@@ -118,6 +119,24 @@ std::string Document_class::font_definitions()
ss << " --sans-serif: \"" << m_resolved_sans.family_name << "\", sans-serif;\n"; ss << " --sans-serif: \"" << m_resolved_sans.family_name << "\", sans-serif;\n";
if (!m_mono_typeface.empty()) if (!m_mono_typeface.empty())
ss << " --monospace: \"" << m_resolved_mono.family_name << "\", monospace;\n"; ss << " --monospace: \"" << m_resolved_mono.family_name << "\", monospace;\n";
// The factor a @typeface span's font-size is multiplied by (2026-09-18).
// font.css renders the body at "font-size-adjust: ex-height 0.5", so
// the text the reader sees is 0.5 / x-height times its nominal size
// (EB Garamond, x-height 0.400: 1.25). A @typeface span switches
// the adjust OFF (a script with no x-height cannot be matched), so
// its ":scale N" was N times the NOMINAL size -- about a fifth
// smaller against the visible text than the same N in pdf, where
// fontspec's Scale= is against text rendered at nominal size.
// Folding the body's enlargement in here makes :scale mean the
// same thing in both targets. Emitted as a custom property the
// span multiplies in with calc(), so a fragment rendered without
// @document falls back to 1. SYNC: the anchor 0.5 is font.css's.
const float xheight_anchor = 0.5f;
if (m_resolved_serif.xheight_ratio > 0.0f) {
std::stringstream factor {};
factor << std::setprecision(4) << xheight_anchor / m_resolved_serif.xheight_ratio;
ss << " --typeface_scale: " << factor.str() << ";\n";
}
ss << "}\n"; ss << "}\n";
} }
// Font-size normalization across families is no longer emitted from // Font-size normalization across families is no longer emitted from
+33
View File
@@ -0,0 +1,33 @@
import kutil
import klammer_base
import phases
class Document(klammer_base.Klammer_base):
def __init__(self, K):
super().__init__(K)
if self.files:
raise Exception("The :files option is not defined for the 'txt' target")
def html(self):
raise Exception("Implemented by document.cpp")
def tex(self):
raise Exception("Implemented by document.cpp")
def txt(self):
# The document marker: section numbering under txt is a document
# service, and the numbering phase numbers only when it finds this
# at the head of the text (a fragment, as kdesc renders, has none).
result = phases.txt_document_marker() + "\n"
if self.title:
result += f"@b {self.title} @ @nl@\n"
if self.subtitle:
result += f"@b {self.subtitle} @ @nl@\n"
result += "\n"
if self.author:
result += f"@i {self.author} @ @nl@\n"
if self.date:
result += f"@i {self.date} @ @nl@\n"
result += "\n"
result += self.text
return result
+29 -4
View File
@@ -26,13 +26,28 @@ font as document text instead of setting anything. @typeface is the
temporary, all-target replacement. temporary, all-target replacement.
]# ]#
@@typeface.k name | text :scale.float 1.0 : @@typeface.k name | text :scale.float 1.0 :middle.bool :lift.length :
A temporary change of typeface for *text*: the typeface *name* is A temporary change of typeface for *text*: the typeface *name* is
resolved in the Klammertext font store (the directories of resolved in the Klammertext font store (the directories of
KLAMMERTEXT_FONTS, then the distribution's own), and the document KLAMMERTEXT_FONTS, then the distribution's own), and the document
declares and localizes it automatically -- writing this klammer is the declares and localizes it automatically -- writing this klammer is the
only declaration needed. *scale* multiplies the surrounding text size, only declaration needed. *scale* multiplies the size of the surrounding
which a script whose glyphs are drawn small usually wants. text as it appears, the same in html and pdf, which a script whose glyphs
are drawn small usually wants.
*middle* centres *text* on the surrounding line instead of standing it on
the baseline -- for a script with no baseline relationship to the text,
such as hieroglyphs, whose font declares a tall ascender that would
otherwise open up every line the glyphs sit in. Html only: in tex a
row is already as tight as its tallest glyph. Never for a Latin
typeface, whose baseline must stay shared with the text around it.
*lift* shifts *text* up by a length (down when negative), in em, pt or
px -- the correction for a font that draws its glyphs off the baseline,
which is a property of the font and so belongs in the klammer that names
it: Noto Sans Egyptian Hieroglyphs floats its signs and wants about
0.15em, NewGardinerOmni stands them on the baseline and wants none.
Html and tex alike; the txt target has no vertical position.
The change ends with *text*: nothing after it is affected. Under the The change ends with *text*: nothing after it is affected. Under the
txt target a typeface cannot be expressed at all, so *text* is emitted txt target a typeface cannot be expressed at all, so *text* is emitted
@@ -107,9 +122,19 @@ than a typeface -- xeCJK for the tex path), so this klammer errors with
the list of available typefaces until one is installed with the list of available typefaces until one is installed with
"kdesc --typeface install". That is the honest behaviour: the klammer "kdesc --typeface install". That is the honest behaviour: the klammer
states what it needs and the store reports what it has. states what it needs and the store reports what it has.
]#
@@cjk.k text : Set *text* in a typeface that supports CJK @@ @@cjk.k text : Set *text* in a typeface that supports CJK @@
@@cjk.html,tex,txt :: @typeface Noto Sans CJK SC | *text* @ @@ @@cjk.html,tex,txt :: @typeface Noto Sans CJK SC | *text* @ @@
]#
# Only defining on Chinese style for now; could be parameterized.
@@chinese.k text :scale.float 1 : Set *text* in a Chinese font @@
@@chinese.html,tex,txt :: @typeface Noto Serif SC :scale *scale* | *text* @ @@
# Egyptian hieroglyphs
@@egyptian.k text :scale.float 1.75 : Set *text* in an Egyptian hieroglyph font @@
@@egyptian.html,tex,txt :: @typeface NewGardinerOmni :scale *scale* :middle | *text* @ @@
# @@egyptian.html,tex,txt :: @typeface Noto Sans Egyptian Hieroglyphs :scale *scale* :middle | *text* @ @@
@@fraktur.k s : Set text *s* in Fraktur @@ @@fraktur.k s : Set text *s* in Fraktur @@
@@fraktur.tex :: \textfrak{*s*} @@ @@fraktur.tex :: \textfrak{*s*} @@
+52 -3
View File
@@ -60,9 +60,38 @@ class Typeface(klammer_base.Klammer_base):
# this sits inside a double-quoted html attribute. What is quoted # this sits inside a double-quoted html attribute. What is quoted
# here is the MARKER: @document replaces it with the resolved # here is the MARKER: @document replaces it with the resolved
# family name (see the marker note above). # family name (see the marker note above).
# The size is the scale times the body's own enlargement: the body
# is rendered at "font-size-adjust: ex-height 0.5" (font.css), so
# the visible text is larger than its nominal size, and a span
# with the adjust off at a plain "Nem" sat about a fifth smaller
# against it than the same :scale in pdf. @document emits the
# factor as --typeface_scale (document_html.cpp, font_definitions);
# the fallback 1 is for a fragment rendered without @document.
# The "*" of calc() survives the @eval re-read (tested 2026-09-18);
# the "--" of the property name is hidden below with the rest.
style = "', serif; font-size-adjust: none" style = "', serif; font-size-adjust: none"
if float(self.scale) != 1.0: style += f"; font-size: calc({float(self.scale)}em * var(--typeface_scale, 1))"
style += f"; font-size: {float(self.scale)}em" # :middle -- for a script with no baseline relationship to the
# text. A browser sizes the span's inline box from the FONT's
# ascender and descender, not the glyph's ink, and centres it on
# the content area's midpoint; a hieroglyph font declares an
# ascender of 1.0 em and no descender (NewGardinerOmni; Noto:
# 1.324/-0.326), so at :scale 1.75 the box sat half an em too
# high and every table row grew from 31 to 47 px (Noto: 62).
# line-height 1 drops the body's leading from the enlarged box and
# vertical-align middle centres it on the PARENT's x-height, which
# is why this uses the text's metrics, not the script font's zeros.
# Measured 2026-09-18: 40 px, the floor the ink allows; line-height
# 0 alone gave 31 px with the glyphs overlapping the rows above.
# Off by default: a Latin typeface must keep the shared baseline.
if self.middle:
style += "; line-height: 1; vertical-align: middle"
if self.lift_length():
# Relative positioning moves the ink and nothing else: the
# line box is laid out as if the span had not moved, which is
# what a baseline correction wants (vertical-align would
# re-lay the line). Negative top = up, as \raisebox in tex.
style += f"; position: relative; top: -{self.lift_length()}"
# The STYLE is markup, so its typographically active characters are # The STYLE is markup, so its typographically active characters are
# hidden: the html target's transforms turn a single quote into a # hidden: the html target's transforms turn a single quote into a
# curly one, which broke the css font-family value. The MARKER is # curly one, which broke the css font-family value. The MARKER is
@@ -81,7 +110,27 @@ class Typeface(klammer_base.Klammer_base):
# makes the change temporary (the retired @font emitted a bare # makes the change temporary (the retired @font emitted a bare
# \setmainfont, which changed the font for the rest of the # \setmainfont, which changed the font for the rest of the
# document). # document).
return "{" + typeface_marker(self.name, self.scale) + " " + self.text + "}" group = "{" + typeface_marker(self.name, self.scale) + " " + self.text + "}"
if self.lift_length():
# A \raisebox produced HERE is safe from the generator escaping
# pass: this klammer is applied after a generator's result has
# been escaped, so its backslash is never swept -- the trap
# @nl.tex fell into (2026-09-07) is a klammer applied inside a
# generator's ARGUMENT, which @typeface shares with @b and @i.
return "\\raisebox{" + self.lift_length() + "}" + group
return group
def lift_length(self):
# :lift, validated to the units both targets can move by. The
# length argtype admits w/h fractions and a quoted string, which
# mean nothing as a vertical shift; a bare number would be columns
# or lines and is refused for the same reason.
lift = self.lift
if not lift:
return ""
if not re.fullmatch(r"-?(\d+\.?\d*|\.\d+)(em|pt|px)", lift):
raise Exception(f"@typeface :lift takes a length in em, pt or px, not '{lift}'")
return lift
def txt(self): def txt(self):
# A typeface cannot be expressed in plain text. Declared in the # A typeface cannot be expressed in plain text. Declared in the
+1 -42
View File
@@ -19,8 +19,7 @@ class Image(klammer_base.Klammer_base):
id = 0 id = 0
def __init__(self, K, as_string=True, width=None): def __init__(self, K, as_string=True, width=None):
super().__init__(K) super().__init__(K)
# pprint.pprint(self.__dict__)
# self.show()
# /dev/shm is a fast RAM-backed tmpfs on Linux; on macOS it does not # /dev/shm is a fast RAM-backed tmpfs on Linux; on macOS it does not
# exist, so fall back to the platform temp directory. # exist, so fall back to the platform temp directory.
cache_dir = "/dev/shm" cache_dir = "/dev/shm"
@@ -48,21 +47,11 @@ class Image(klammer_base.Klammer_base):
def html(self): def html(self):
img_dir = f"{self.K_output_dir}/{self.K_output_basename}/{self.Image_output_dir}" img_dir = f"{self.K_output_dir}/{self.K_output_basename}/{self.Image_output_dir}"
# output_dir = self.K_output_dir or "."
# input_basename = self.__dict__.get("K_input_name", "")
# input_basename = "/" + input_basename if input_basename else ""
# img_dir = f"{output_dir}/{self.image_output_dir}{input_basename}"
#print("img_dir:", img_dir)
if not os.path.exists(img_dir): if not os.path.exists(img_dir):
os.makedirs(img_dir) os.makedirs(img_dir)
if not os.path.exists(f"{img_dir}/{os.path.basename(self.source)}"): if not os.path.exists(f"{img_dir}/{os.path.basename(self.source)}"):
shutil.copy2(self.source, img_dir) shutil.copy2(self.source, img_dir)
# width, scale_x, scale_y = kutil.parse_length(
# "html",
# self.width if self.width != "none" else self.image_default_width)
scale_x, scale_y = 1, 1 # Old scale_x, scale_y = 1, 1 # Old
result = E("img") result = E("img")
@@ -72,9 +61,6 @@ class Image(klammer_base.Klammer_base):
filename = f"{self.Image_output_dir}/{os.path.basename(self.source)}" filename = f"{self.Image_output_dir}/{os.path.basename(self.source)}"
result.attr("src", filename).attr("alt", os.path.basename(filename)) result.attr("src", filename).attr("alt", os.path.basename(filename))
#if self.width:
# kutil.msg("rel: " + str(self.rel_fraction))
length, value, ltype = kutil.parse_width("html", self.width, self.rel_fraction) length, value, ltype = kutil.parse_width("html", self.width, self.rel_fraction)
if ltype == "w": if ltype == "w":
@@ -90,19 +76,6 @@ class Image(klammer_base.Klammer_base):
if self.border: if self.border:
result.cls("image_border") result.cls("image_border")
# print(result)
#result.attr("style", f"width: {width}")
# if width and width != "none":
# result.attr("style", f"width: {width}")
# if width == "none":
# if scale_x:
# result.attr("style", "width: 0px;")
# result.attr("scale-x", scale_x)
# elif scale_y:
# result.attr("style", "height: 0px;")
# result.attr("scale-y", scale_y)
if self.file_error: if self.file_error:
self.caption += self.file_error_message self.caption += self.file_error_message
@@ -120,14 +93,7 @@ class Image(klammer_base.Klammer_base):
def tex(self): def tex(self):
# kutil.msg(f"number: {self.number}")
#selfsource, width, height = self.cache.get(self.K_target, self.basename)
source = os.path.abspath(self.source) source = os.path.abspath(self.source)
#if self.abswidth != 0:
# width = f"{self.abswidth}\\paperwidth"
#if self.caption or self.number:
# width = "\\textwidth"
#else:
width = kutil.parse_width("tex", self.width, self.rel_fraction)[0] width = kutil.parse_width("tex", self.width, self.rel_fraction)[0]
width = re.sub("px", "pt", width) width = re.sub("px", "pt", width)
#result = f'\\includegraphics[width={width}]{{{source}}}' #result = f'\\includegraphics[width={width}]{{{source}}}'
@@ -138,8 +104,6 @@ class Image(klammer_base.Klammer_base):
result = latex_util.add_caption( result = latex_util.add_caption(
result, "Figure", self.number, self.caption, width, result, "Figure", self.number, self.caption, width,
self.hpos, self.caption_side, offset=self.offset) self.hpos, self.caption_side, offset=self.offset)
#result, "Figure", self.number, self.caption, self.hpos, self.caption_side,
#self.width, self.caption_side_center, self.vmargin, self.caption_margin)
else: else:
result = f'\\includegraphics[width={width}]{{{source}}}' result = f'\\includegraphics[width={width}]{{{source}}}'
result = latex_util.caption_wrapper(result, self.hpos, offset=self.offset) result = latex_util.caption_wrapper(result, self.hpos, offset=self.offset)
@@ -150,8 +114,3 @@ class Image(klammer_base.Klammer_base):
# box material: it must sit in vertical mode or it is typeset beside # box material: it must sit in vertical mode or it is typeset beside
# any text it follows. # any text it follows.
return latex_util.block(result) return latex_util.block(result)
# Better to cause an error; there's no good text substitute for an image
# def txt(self):
# return f"[Image: {self.basename}\nCaption: {self.caption}]"
-8
View File
@@ -1,4 +1,3 @@
import pprint
import re import re
import kutil import kutil
@@ -41,25 +40,18 @@ def hide_typographic(s):
class Klammer_base: class Klammer_base:
def __init__(self, K): def __init__(self, K):
#args = {k:escape(v) for k, v in K.__dict__.items()
args = {k:v for k, v in K.__dict__.items() args = {k:v for k, v in K.__dict__.items()
if not k.startswith('__')} if not k.startswith('__')}
for key in args: for key in args:
setattr(self, key, args[key]) setattr(self, key, args[key])
#setattr(self, "_klammer_name", klammer_name)
#pprint.pprint(self.__dict__)
def html(self): def html(self):
#return '[{}: HTML]'.format(self.__class__.__name__)
return None return None
def tex(self): def tex(self):
#return '[{}: LaTeX]'.format(self.__class__.__name__)
return None return None
def txt(self): def txt(self):
#return '[{}: Plain text]'.format(self.__class__.__name__)
return None return None
def show(self, label=""): def show(self, label=""):
+2 -1
View File
@@ -29,7 +29,8 @@ class List(klammer_base.Klammer_base):
body = "\n\n".join([f"\\item {e}" for e in self.items]) body = "\n\n".join([f"\\item {e}" for e in self.items])
command = {'ol' : 'enumerate', 'ul' : 'itemize'}[self.list_type] command = {'ol' : 'enumerate', 'ul' : 'itemize'}[self.list_type]
init = "" init = ""
if self.__dict__.get("initial") and self.initial != 1: # :initial is @ol's alone (list.k); a @ul has no such attribute.
if getattr(self, "initial", 1) != 1:
init = "\\addtocounter{enumi}{" + str(self.initial - 1) + "}" init = "\\addtocounter{enumi}{" + str(self.initial - 1) + "}"
#topsep = '[noitemsep,topsep=0pt]' #topsep = '[noitemsep,topsep=0pt]'
topsep = "[noitemsep,topsep=0pt," if self.compressed else "[topsep=0pt,itemsep=2pt," topsep = "[noitemsep,topsep=0pt," if self.compressed else "[topsep=0pt,itemsep=2pt,"
+13 -8
View File
@@ -1,6 +1,11 @@
# Book-specific component headings (others possible to correspond to # Book-specific component headings (others possible to correspond to
# the front and back matter of a book): # the front and back matter of a book):
@@section_args.o :id :n.int -1 :
Common arguments for section headings. If *n* is not the default, it is used
as the section number.
@@
@@preface.k :id :name Preface : Preface text @@ @@preface.k :id :name Preface : Preface text @@
@@preface.html,tex,txt :: @eval section.Section(K, "preface") eval@ @@ @@preface.html,tex,txt :: @eval section.Section(K, "preface") eval@ @@
@@ -9,30 +14,30 @@
@@part.k title :id :name Part : Part heading @@ @@part.k title :id :name Part : Part heading @@
@@part.html,tex,txt :: @eval section.Section(K, 0) eval@ @@ @@part.html,tex,txt :: @eval section.Section(K, 0) eval@ @@
@@s1.k title :id :n : Top level division, numbered @@ @@s1.k title @section_args@ : Top level division, numbered @@
@@s1.html,tex,txt :: @eval section.Section(K, 1) eval@ @@ @@s1.html,tex,txt :: @eval section.Section(K, 1) eval@ @@
@@s2.k title :id :n : Second level division, numbered @@ @@s2.k title @section_args@ : Second level division, numbered @@
@@s2.html,tex,txt :: @eval section.Section(K, 2) eval@ @@ @@s2.html,tex,txt :: @eval section.Section(K, 2) eval@ @@
@@s3.k title :id :n : Third level division, numbered @@ @@s3.k title @section_args@ : Third level division, numbered @@
@@s3.html,tex,txt :: @eval section.Section(K, 3) eval@ @@ @@s3.html,tex,txt :: @eval section.Section(K, 3) eval@ @@
@@s4.k title :id :n : Fourth level division, numbered @@ @@s4.k title @section_args@ : Fourth level division, numbered @@
@@s4.html,tex,txt :: @eval section.Section(K, 4) eval@ @@ @@s4.html,tex,txt :: @eval section.Section(K, 4) eval@ @@
@@s5.k title :id :n : Fifth level division, numbered @@ @@s5.k title @section_args@ : Fifth level division, numbered @@
@@s5.html,tex,txt :: @eval section.Section(K, 5) eval@ @@ @@s5.html,tex,txt :: @eval section.Section(K, 5) eval@ @@
@@s6.k title :id :n : Sixth level division, numbered @@ @@s6.k title @section_args@ : Sixth level division, numbered @@
@@s6.html,tex,txt :: @eval section.Section(K, 6) eval@ @@ @@s6.html,tex,txt :: @eval section.Section(K, 6) eval@ @@
@@s7.k title :id :n : Seventh level division, numbered @@ @@s7.k title @section_args@ : Seventh level division, numbered @@
@@s7.html,tex,txt :: @eval section.Section(K, 7) eval@ @@ @@s7.html,tex,txt :: @eval section.Section(K, 7) eval@ @@
# Unnumbered section headings: # Unnumbered section headings:
@@h1.k title :id :n : Top level division, unnumbered @@ @@h1.k title :id : Top level division, unnumbered @@
@@h1.html,tex,txt :: @eval section.Section(K, 1, numbered=False) eval@ @@ @@h1.html,tex,txt :: @eval section.Section(K, 1, numbered=False) eval@ @@
@@h2.k title :id : Second level division, unnumbered @@ @@h2.k title :id : Second level division, unnumbered @@
+16 -7
View File
@@ -1,5 +1,6 @@
import kutil import kutil
import klammer_base import klammer_base
import phases
NUMBER_VAR = "___NUM___" NUMBER_VAR = "___NUM___"
@@ -48,7 +49,6 @@ class Section(klammer_base.Klammer_base):
# names = "part chapter section subsection subsubsection paragraph subparagraph subsubparagraph subsubsubparagraph".split() # names = "part chapter section subsection subsubsection paragraph subparagraph subsubparagraph subsubsubparagraph".split()
names = "part sA sB sC sD sE".split() names = "part sA sB sC sD sE".split()
#self.show()
if self.id: if self.id:
id = self.id id = self.id
else: else:
@@ -56,13 +56,8 @@ class Section(klammer_base.Klammer_base):
Section.label_number += 1 Section.label_number += 1
result = '' result = ''
if self.__dict__.get("n") and self.n: if getattr(self, "n", -1) > -1: # absent for @part and @h1..@h7
result += f'\\setcounter{{chapter}}{{{int(self.n)-1}}}\n' result += f'\\setcounter{{chapter}}{{{int(self.n)-1}}}\n'
#if self.id:
# result +=
#if self.level == 1:
#result += '\\newpage\n\n'
# result += '\\vspace*{24pt}'
suffix = "" if self.numbered else "*" suffix = "" if self.numbered else "*"
result += f'\\{names[self.level]}{suffix}{{{self.title}}}' result += f'\\{names[self.level]}{suffix}{{{self.title}}}'
result += f'\n\\hypertarget{{{id}}}{{}}' result += f'\n\\hypertarget{{{id}}}{{}}'
@@ -73,4 +68,18 @@ class Section(klammer_base.Klammer_base):
return result return result
def txt(self): def txt(self):
# The number is a marker phases.txt_insert_section_numbers resolves
# -- to a number inside a @document, to nothing outside one -- so
# the space after it is the marker's, not the title's. A preface
# and an unnumbered heading carry no marker at all; a part carries
# a level-0 marker and its name, as the html "Part" prefix.
if self.preface:
return f"@b {self.name} @"
if not self.numbered:
return f"@b {self.title} @" return f"@b {self.title} @"
# :n comes from section_args, which @part and the @h headings do
# not declare, so it is absent there rather than -1.
marker = phases.txt_section_marker(self.level, getattr(self, "n", -1))
if self.level == 0:
return f"@b {marker} {self.name} {self.title} @"
return f"@b {marker} {self.title} @"
+7 -2
View File
@@ -12,8 +12,13 @@ tr:first-child {
td { td {
/* padding-left: 1rem; */ /* Vertical space is the table's line-height (:leading, set on the
padding: .1rem .5rem .2rem .5rem; <table> by table.py -- the html equivalent of \arraystretch), so no
vertical padding here: it was an html-only extra that kept rows
looser than the pdf's. Horizontal padding is :colsep, the same
value that becomes \tabcolsep; the fallback is for a table that
reaches this rule without the property. */
padding: 0 var(--colsep, 4pt);
vertical-align: top; vertical-align: top;
} }
+20 -2
View File
@@ -575,6 +575,24 @@ class Table(klammer_base.Klammer_base):
# would wrap it as a stray <p> inside the table. # would wrap it as a stray <p> inside the table.
return E("colgroup").body(cols.strip(), newline=False).str(), layout, table_width return E("colgroup").body(cols.strip(), newline=False).str(), layout, table_width
def html_style(self):
# :leading and :colsep under html (2026-09-18; both were tex-only).
# \arraystretch multiplies the row strut, which is \baselineskip,
# 1.2 times the font size -- so :leading L is a 1.2 L em row, and
# the html equivalent is that line-height on the table, inherited
# by every cell. The cells' vertical padding went with it
# (table.css): an html-only extra with no tex counterpart, which
# kept a :leading 1 table 0.3rem looser per row than its pdf.
# :colsep is a custom property the td rule reads for its horizontal
# padding (padding does not inherit); the flush-edge classes still
# zero the outer ones. Measured 2026-09-18: rows of :leading 1 and
# :leading 2 in the ratio 1:2 in both targets.
line_height = f"{1.2 * float(self.leading):.4g}"
# Hidden: the result is re-read and the html target's transforms
# would turn the property's "--" into an en dash (font.py's trap).
return klammer_base.hide_typographic(
f"line-height: {line_height}; --colsep: {self.colsep}")
def html(self): def html(self):
result = "" result = ""
for row_i, row in enumerate(self.cells): for row_i, row in enumerate(self.cells):
@@ -592,7 +610,7 @@ class Table(klammer_base.Klammer_base):
# algorithm), matching the tex \ratio division. The max-width # algorithm), matching the tex \ratio division. The max-width
# cap goes on the caption wrapper when there is one (the table's # cap goes on the caption wrapper when there is one (the table's
# own percentage would be circular in a shrink-to-fit wrapper). # own percentage would be circular in a shrink-to-fit wrapper).
result = E("table").body(result) result = E("table").body(result).attr("style", self.html_style())
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, result, "Table", self.number, self.caption,
@@ -605,7 +623,7 @@ class Table(klammer_base.Klammer_base):
result = html_util.hpos_container(result, self.hpos, self.offset).str() result = html_util.hpos_container(result, self.hpos, self.offset).str()
return result return result
colgroup, layout, table_width = self.html_colgroup() colgroup, layout, table_width = self.html_colgroup()
result = E("table").body(colgroup + result) result = E("table").body(colgroup + result).attr("style", self.html_style())
if self.number or self.caption: if self.number or self.caption:
# The caption wrapper carries the width (a percentage on the # The caption wrapper carries the width (a percentage on the
# shrink-to-fit wrapper itself would be circular) and the table # shrink-to-fit wrapper itself would be circular) and the table
+44
View File
@@ -248,6 +248,50 @@ def txt_format_footnotes(text, K):
+ label + " " + footnote + "\n\n") + label + " " + footnote + "\n\n")
return result return result
# Section numbering under txt (2026-09-18). Numbering is a DOCUMENT
# service, as it is in html (a pass @document runs over the page) and tex
# (LaTeX's own counters inside the document): a heading rendered outside
# a @document -- kdesc's renders of the ref/ generators -- is not
# numbered. Document.txt() (sks/document/document_txt.py) opens its
# result with the document marker; Section.txt() emits a section marker
# carrying the level and the :n override; this phase, first in the txt
# and ansi lists, numbers the headings when the document marker is there
# and strips every marker either way. A heading at a level with no
# parent numbers as "0.1", as html and tex print it -- the txt target
# does not diverge from them. The single-source functions are the
# TODO #47 pattern.
def txt_document_marker():
return "__KTEXTDOCUMENT__"
def txt_section_marker(level, n=-1):
# LEVEL 0 is a part, 1-7 the sections; N > -1 sets the number.
return f"__KTEXTSECTION__{level}__{n}__"
def txt_insert_section_numbers(text, K):
# The marker is the first thing the document emits, but the result
# reaches the phase behind whatever newlines the apply left in front.
head = text.lstrip()
in_document = head.startswith(txt_document_marker())
if in_document:
text = head[len(txt_document_marker()):].lstrip("\n")
levels = [0] * 8 # 0: parts; 1-7: sections (parts do not reset them, as in LaTeX)
marker_rgx = re.compile(r"__KTEXTSECTION__(\d+)__(-?\d+)__ ?")
def number(match):
if not in_document:
return ""
level, n = int(match.group(1)), int(match.group(2))
levels[level] = n if n > -1 else levels[level] + 1
if level == 0:
return f"{levels[0]} " # a part resets nothing below it
for i in range(level + 1, len(levels)):
levels[i] = 0
return ".".join(str(v) for v in levels[1:level + 1]) + " "
return marker_rgx.sub(number, text)
# HTML
def html_footnote_marker(): def html_footnote_marker():
return "kt-footnote" return "kt-footnote"
+2
View File
@@ -63,6 +63,7 @@ of this: neither the transforms nor the quoting reach it.
' ' | ' ' |
~ __KTEXTNBSP__ ~ __KTEXTNBSP__
:after_apply :after_apply
phases.txt_insert_section_numbers ;
phases.txt_format_footnotes ; phases.txt_format_footnotes ;
phases.txt_insert_captions ; phases.txt_insert_captions ;
phases.txt_justify_blocks ; phases.txt_justify_blocks ;
@@ -83,6 +84,7 @@ of this: neither the transforms nor the quoting reach it.
@@@target ansi | Plain text with ANSI escape sequences for type styles @@@target ansi | Plain text with ANSI escape sequences for type styles
:includes txt :includes txt
:after_apply :after_apply
phases.txt_insert_section_numbers ;
phases.txt_format_footnotes ; phases.txt_format_footnotes ;
phases.txt_insert_captions ; phases.txt_insert_captions ;
phases.txt_justify_blocks ; phases.txt_justify_blocks ;
+6 -1
View File
@@ -8,7 +8,7 @@
# * a flag a user reaches for often gets a single letter (-k klammers, # * a flag a user reaches for often gets a single letter (-k klammers,
# -t targets, -c character codes, -i input); a more specialised topic gets # -t targets, -c character codes, -i input); a more specialised topic gets
# a multi-letter name (--argtypes, --katoms, --rewrite, --optionsets, # a multi-letter name (--argtypes, --katoms, --rewrite, --optionsets,
# --coverage, --klammersets, --font); # --coverage, --klammersets, --typeface);
# * -v says how much to show about the command's PROCESSING and never what # * -v says how much to show about the command's PROCESSING and never what
# its RESULT contains. So the katom regex column is "--katoms full" and # its RESULT contains. So the katom regex column is "--katoms full" and
# the coverage file column is "--coverage all", not verbosity levels. # the coverage file column is "--coverage all", not verbosity levels.
@@ -151,6 +151,11 @@ shows "24. --coverage help explains" 'Coverage commands' "$KDESC" --cover
shows "25. --katoms help explains" 'Katom commands' "$KDESC" --katoms help shows "25. --katoms help explains" 'Katom commands' "$KDESC" --katoms help
shows "26. an unknown coverage word" 'Unrecognized coverage command' "$KDESC" --coverage nonsense shows "26. an unknown coverage word" 'Unrecognized coverage command' "$KDESC" --coverage nonsense
shows "27. an unknown katom word" 'Unrecognized katom command' "$KDESC" --katoms nonsense shows "27. an unknown katom word" 'Unrecognized katom command' "$KDESC" --katoms nonsense
# The help text and the error must name the flag by its CURRENT spelling:
# both still said --font for three weeks after the 2026-08-25 rename, and # retired-ok
# the retired-name guard's entries did not match the bare word.
shows "27a. --typeface help names --typeface install" '--typeface install' "$KDESC" --typeface help
shows "27b. an unknown typeface word names the flag" 'Unrecognized typeface command: --typeface' "$KDESC" --typeface nonsense
echo echo
echo "-- provenance: -i shows what the INPUT defines --" echo "-- provenance: -i shows what the INPUT defines --"