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)
250 lines
8.0 KiB
Python
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]
|