Assembled from klammertext-dev f344a37d0f6e. The commands, the library,
the Standard Klammer Set, the reflection klammerset and the Python
binding build and run natively on Windows (MSYS2 UCRT64 gcc, python.org
Python), and both test tiers pass there. doc/install/windows_source_install.md
is the fifth install guide: a source install parallel to the Linux and
macOS ones, verified by following it on a pristine Windows 11.
Behaviour on every system: paths Klammertext prints use forward slashes;
output is LF; the command line is UTF-8 on Windows; @eval :shell is a
POSIX shell command everywhere -- on Windows it runs the sh Klammertext
ships, BusyBox for Windows (win/busybox/, GPLv2, with its licence and the
source pointer). KLAMMERTEXT_FONTS and KLAMMERTEXT_KLAMMERSETS are lists in
the system's PATH convention (';' on Windows). On Linux and macOS nothing
observable changes.
297 lines
10 KiB
Python
297 lines
10 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):
|
|
# Forward slashes: the path ends up in document text (\includegraphics,
|
|
# an html src), where a Windows backslash is a TeX command or an escape.
|
|
directory = relative_pathname.replace(os.sep, "/")
|
|
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=1, axis="width"):
|
|
"""A length argument as the target's own length.
|
|
|
|
Returns (text, number, unit). `axis` is "width" or "height" and gives
|
|
a BARE number its meaning -- characters or lines, the natural unit of
|
|
the axis in every target (sks/kutil/kutil.k, the width and height
|
|
argtypes): under txt the integer count itself; under html "ch" and
|
|
"lh" (a consumer of "lh" writes the em fallback first, see
|
|
css_height); under tex the width of a digit in the current font
|
|
(\\fontcharwd) and \\baselineskip. `target` "txt" is accepted for the
|
|
bare units only, since plain text has no other.
|
|
"""
|
|
def choose(html_value, tex_value, txt_value=None):
|
|
if target == "txt" or target == "ansi":
|
|
if txt_value is None:
|
|
unit = "characters" if axis == "width" else "lines"
|
|
raise Exception(f'The {axis} "{s}" has no meaning in plain text: '
|
|
f'give a bare number of {unit}')
|
|
return txt_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 == "":
|
|
# The natural unit of the axis.
|
|
if axis == "height":
|
|
result = choose(f"{num}lh", f"{num}\\baselineskip", str(round(num)))
|
|
units = "lines"
|
|
else:
|
|
result = choose(f"{num}ch", f"{num}\\fontcharwd\\font`0 ", str(round(num)))
|
|
units = "ch"
|
|
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:
|
|
# em, pt: the same text in html and tex; none in plain text.
|
|
result = choose(f"{num}{units}", f"{num}{units}")
|
|
result = re.sub("\t", "\\t", result)
|
|
return result, num, units
|
|
|
|
|
|
def parse_width(target, s, rel_fraction=1):
|
|
return parse_length(target, s, rel_fraction, "width")
|
|
|
|
def parse_height(target, s, rel_fraction=1):
|
|
return parse_length(target, s, rel_fraction, "height")
|
|
|
|
def css_height(prop, value):
|
|
"""A CSS declaration for a height from parse_height's html text. The
|
|
"lh" unit is recent (2023 in every major browser); an older browser
|
|
ignores a declaration it cannot parse and keeps the previous one, so
|
|
the em equivalent is written FIRST and lh second -- the standard
|
|
fallback idiom, no detection needed."""
|
|
if value.endswith("lh"):
|
|
# A short decimal, not the float's full expansion (1.5 * 1.2 is
|
|
# 1.7999999999999998 in binary).
|
|
em = f"{float(value[:-2]) * 1.2:.4g}"
|
|
return f"{prop}: {em}em; {prop}: {value}"
|
|
return f"{prop}: {value}"
|
|
|
|
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]
|