Files
klammertext/sks/kutil/kutil.py
T
ack b80e823ab3 Tables in the plain-text target, with captions
A @table now renders to txt.  What it needed settled how plain text sets
a block off from the prose around it, which is by INDENTING it: that is
the only structural device the medium has, where html and LaTeX have
boxes, rules and space.  An indented block is one that has already been
laid out, and the target leaves its lines alone.

A caption is placed above a table or a listing and below a figure, in
every target rather than only in plain text.  A figure is taken in at a
glance, and the caption then names what the reader has already seen; a
table is read, and a reader who meets the rows before the subject has to
read them twice.

Also fixed, all three of them silent: an indented paragraph was dropped
from plain-text output rather than left as written; an @indent block was
re-flowed, so the indentation of its later lines appeared as gaps in the
middle of a line; and a table captioned with ":number false" lost the
rule under its caption, while an unnumbered table wrongly consumed a
number that a reader counting captions would then find missing.

(from dev 8173c371bba6)
2026-08-26 23:00:58 +02:00

250 lines
8.0 KiB
Python

import os
import re
import inspect
import html_util
from html_util import E
def black_text():
return "\033[0;30m"
def blue_text(text):
return f"\033[0;34m{text}\033[0m"
def red_text(text):
return f"\033[31m{text}\033[0m"
def msg(text=""):
frame = inspect.currentframe().f_back
print(blue_text(f"[{os.path.basename(frame.f_code.co_filename)}:{frame.f_lineno}]"), text)
def escape(s):
result = s
# These were commented out -- has it been replaced?
# It's only used in kutil/klammer_base.py and block/block.py
# and does nothing now.
#result = re.sub(r"\b", r"\b", result)
#result = re.sub(r"\t", r"\t", result)
#result = re.sub("\f", r"\\f", result)
#result = re.sub("\v", r"\\v", result)
#result = re.compile("\\(.)").sub(r"\\\1", result)
return result
def klammertext_dir():
var = 'KLAMMERTEXT_HOME'
kdir = os.environ.get(var)
if kdir is None:
raise Exception(
f"The environment variable {var} must be defined as the top-level Klammertext directory")
return kdir
def cache_directory(relative_pathname, basename):
directory = relative_pathname
if not os.path.isdir(directory):
os.makedirs(directory)
result = directory + "/_klammertext_cache/" + basename
return result;
def sks_dirs():
result = 'kutil book document block font link section list image table code color'.split()
k = klammertext_dir()
result = [[e, f"{k}/sks/{e}"] for e in result]
return result
def sks_files_of_type(extension):
result = []
for base, sks_dir in sks_dirs():
filename = f'{sks_dir}/{base}.{extension}'
if os.path.exists(filename):
result.append([base, filename])
return result
def make_dir_if_necessary(d, delete_contents=False):
if not os.path.exists(d):
os.makedirs(d)
if delete_contents:
os.system(f'rm -rf {d}/*')
def protect_klammertext_special_characters(text):
result = text
result = re.compile(r"\^").sub("H̅", result)
result = re.compile(r"@").sub("A̅", result)
result = re.compile(r":").sub("̅C̅", result)
result = re.compile(r"\|").sub("̅B̅", result)
return result
def caption_delimiter():
return "__CAPTION__"
def caption_marker(name, caption, label_delimiter=" -- "):
caption = re.sub("\n", " ", caption)
d = caption_delimiter()
if caption and label_delimiter:
caption = f"{label_delimiter}{caption}"
result = f"{d}{name}{d}{caption}{d}"
return result
def rest_split(s, dimensions=1):
"""Split bar-delimited text into nested lists, one level per dimension.
The delimiter for dimension n is a run of exactly n bar characters:
| separates elements, || separates lists of elements, ||| lists of
lists, and so on. This is the cast behind the rest(N) argument type.
One trailing top-level delimiter (the customary dangling separator
before a closing @) is removed; all other empty elements are
preserved, so a trailing | still makes an empty final cell.
"""
s = s.strip()
if not s:
return [] if dimensions > 0 else s
delimiter = "|" * dimensions
if s.endswith(delimiter) and not s.endswith("|" + delimiter):
s = s[:-len(delimiter)]
return _rest_split_level(s, dimensions)
def _rest_split_level(s, dimensions):
if dimensions <= 0:
return s.strip()
pattern = re.compile("(?<!\\|)" + "\\|" * dimensions + "(?!\\|)")
return [_rest_split_level(part, dimensions - 1) for part in pattern.split(s)]
def rest_args(s, dimensions=1):
return rest_split(s, dimensions)
def parse_length(target, s, rel_fraction):
def choose(html_value, tex_value):
return html_value if target == "html" else tex_value
pat = re.compile(r'(?:([0-9.]+)([a-z]+))|(none|f)|(?:"([^"]+)")')
#pat = re.compile(r'(?:([0-9.]+)([a-z]+))|(none|f)|(?:{([^}]+)})')
#pat = re.compile(r'(?:([0-9.]+)([a-z]+))|(f)')
match = pat.match(s)
if match is None: # But already checked by the klammer
raise Exception(f'The argument "{s}" is not a length')
#print(match.groups())
num, units, fit, text = match.groups()
num = float(num) if num else ""
num *= rel_fraction
if fit:
result = "f" # choose("100vw", "\\textwidth")
elif text:
text = re.compile(r"\{\}\\textbackslash\{\}").sub(r"\\", text)
if text[0] == "-":
result = choose("", f"\\textwidth - \\widthof{{ {text[1:]}}}")
else:
result = choose("", f"\\widthof{{ {text}}}")
elif units == "w":
result = choose(f"{100 * num}vw", f"{num}\\textwidth")
elif units == "h":
result = choose(f"{100 * num}vh", f"{num}\\textheight")
elif units == "pw":
result = choose(f"{100 * num}vw", f"{num}\\paperwidth")
elif units == "ph":
result = choose(f"{100 * num}vh", f"{num}\\paperheight")
elif units == "px":
result = choose(f"{round(num)}", f"{num}px")
else:
result = f"{num}{units}"
result = re.sub("\t", "\\t", result)
return result, num, units
def old_parse_length(target, s):
pat = re.compile(r'(?:([0-9.]+)([a-z]+))|(none|f)|("([^"]+)")')
match = pat.match(s)
if match is None:
raise Exception(f'The argument "{s}" is not a length')
if match.group(3) in {"none", "f"}:
return "f"
if s[0] == '"':
if target in {"tex", "pdf"}:
return f"\\widthof{{{match.group(5)} }}"
else:
return ""
num, units = match.groups()[:2]
if float(num) == 0:
return None
result = f"{num}{units}"
if target in {"tex", "pdf"}:
if units == "w":
result = fr"{num}\textwidth"
elif units == "h":
result = fr"{numb}\textheight"
elif target in {"html"}:
scale_x = None
scale_y = None
if units == "w":
#result = fr"{int(float(num)*100)}vw"
result = fr"calc({int(float(num)*100)}vw - 2rem)"
#result = 0
scale_x = num
result = "none"
elif units == "h":
result = fr"{int(float(num)*100)}vw"
#result = 0
scale_y = num
result = "none"
result = [result, scale_x, scale_y]
else:
raise Exception(f"Uknown length: '{s}'")
return result
def parse_lengths(target, s):
# Fill strings with space character for the split:
#print("parse_lengths:", s)
def replace(match):
return re.sub(r"\s", "~", match.group(1))
result = re.compile(r'("[^"]+")').sub(replace, s)
#print(result, result.split())
result = [parse_length(target, e)[0] for e in result.split()]
return result
paragraph_separator_re = re.compile(r'\n *\n', re.S)
def format_for_paragraphs(s):
result = s
if len(paragraph_separator_re.findall(result)) > 0:
result = '\n\n{}\n\n'.format(result)
return result
def filename_list(text):
"""Split a filename list; the Python twin of resolve_filename_list()
in mac/file.cpp (keep the two in sync). A standalone "/" (whitespace
on both sides) separates names, whose inner spacing is preserved;
without a separator, whitespace-separated tokens that do not name
existing files are rejoined with their neighbors into names that do.
A leading ~ expands to the home directory."""
text = text.strip()
if not text:
return []
parts = re.split(r'(?:^|(?<=\s))/(?:\s|$)', text)
if len(parts) > 1:
return [os.path.expanduser(p.strip()) for p in parts if p.strip()]
tokens = text.split()
result, i = [], 0
while i < len(tokens):
if os.path.isfile(os.path.expanduser(tokens[i])):
result.append(tokens[i])
i += 1
continue
acc, j, found = tokens[i], i + 1, False
while j < len(tokens):
acc += " " + tokens[j]
j += 1
if os.path.isfile(os.path.expanduser(acc)):
result.append(acc)
i = j
found = True
break
if not found:
result.append(tokens[i])
i += 1
return [os.path.expanduser(p) for p in result]