Files
klammertext/sks/target/phases.py
T

142 lines
5.9 KiB
Python
Raw Normal View History

import re
import textwrap
import kutil
2026-07-25 21:16:21 +02:00
# 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.
def txt_nl_marker():
return "__KTEXTNEWLINE__"
def txt_vspace_marker():
return "__KTEXTVSPACE__"
def txt_justify_blocks(text, K):
delim = '__DIVIDE__'
2026-07-25 21:16:21 +02:00
text = re.sub("\n\n+", delim, text)
result = ""
for par in text.split(delim):
2026-07-25 21:16:21 +02:00
stripped = par.strip()
n = stripped.count(txt_vspace_marker())
if n and stripped == txt_vspace_marker() * n:
2026-07-25 21:16:21 +02:00
# A paragraph of only @vspace markers: n blank lines in addition
# to the normal paragraph separation.
result += "\n" * n
continue
# A paragraph BEGINNING WITH WHITESPACE is verbatim: it is filled to
# no width and keeps the line breaks it was written with. The same
# in-text rule the error messages use (justify() in mac/util.cpp),
# and the one a klammer relies on to emit lines that must not
# re-flow -- a @table's rows, a listing.
#
# "~" counts as that whitespace, and MUST: a klammer's indent cannot
# be a real space, because Eval::eval TRIMS the result katoms, so
# the leading spaces a klammer emits never reach this pass at all
# (the "~" is the non-breaking space marker, resolved to a space at
# the end of this function -- the only space that survives). A
# writer's own "~" has become a real space long before this, in the
# target's transform table, so there is no ambiguity here.
#
# The emission below was INSIDE this "if" until 2026-08-26, so a
# verbatim paragraph was not left alone, it was DELETED: the one
# branch that had nothing to do also did not copy the text through.
# An indented paragraph vanished from the output with no error.
if par and par[0] not in ' ~':
label = re.match(r"~*\[\d+\] ", par)
par = "\n".join(textwrap.wrap(
par, width=int(K.Target_txt_width), break_long_words=False,
subsequent_indent=" " * len(label.group(0)) if label else ""))
result += par + "\n\n"
result = re.sub(" *" + txt_vspace_marker() + " *", "\n", result)
result = re.sub(r"\n? *" + txt_nl_marker() + r" *\n?", "\n", result)
result = re.sub("~", " ", result)
return result
def txt_insert_captions(text, K):
mark = kutil.caption_delimiter()
caption_rgx = re.compile(f"{mark}(.*?){mark}(.*?){mark}", re.S)
caption_number = 0
width = int(K.Target_txt_width)
indent = int(K.Target_txt_indent)
indented_width = width - indent
def replace(m):
nonlocal caption_number
name, text = m.groups()
# An empty label means an unnumbered caption (":number false"). It
# must NOT take a number from the sequence: a reader counting the
# captions they can see would then find the next one a number
# ahead. The same is true of html and tex, where an unnumbered
# caption emits no marker and so never reaches their counter.
if name:
caption_number += 1
caption = f"{name} {caption_number} - {text}"
else:
caption = text
caption_width = len(caption) + indent
if len(caption) > indented_width:
caption = textwrap.wrap(
caption, indented_width,
#initial_indent ="~"*indent,
subsequent_indent="~"*indent)
caption = "\n".join(caption)
caption_width = indented_width
line_length = max([len(e) for e in caption.split("\n")])
line = "~" * indent + "-" * line_length
return caption + "\n" + line
result = caption_rgx.sub(replace, text)
return result
def txt_footnote_marker():
return "KTEXTFOOTNOTE"
def txt_format_footnotes(text, K):
footnote_number = 0
footnotes = []
def replace(m):
nonlocal footnote_number, footnotes
footnote_number += 1
footnotes.append(m.group(1))
return f"[{footnote_number}]"
start_mark = "__" + txt_footnote_marker()
end_mark = txt_footnote_marker() + "__"
footnote_rgx = re.compile(rf"\s*{start_mark}\s+(.*?)\s+{end_mark}", re.S)
result = footnote_rgx.sub(replace, text).rstrip() + "\n\n"
if footnotes:
rule_count = int(round(float(K.Target_txt_width) * 0.4))
result += "-" * rule_count + "\n\n"
# Calculate maximum width for right-justified numbers:
width = len(f"[{len(footnotes)}]")
for n, footnote in enumerate(footnotes, 1):
label = f"[{n}]"
result += "~" * (width - len(label)) + label + " " + footnote + "\n\n"
return result
def html_footnote_marker():
return "kt-footnote"
def html_format_footnotes(text, K=None):
fnum = 0
footnotes = []
def replace(m):
nonlocal fnum, footnotes
fnum += 1
footnotes.append(m.group(1))
href = f'href ="#_footnote_{fnum}"'
id = f'id="_footnote_src_{fnum}"'
return f'<a {id} {href}><sup class="footnote_in_text">{fnum}</sup></a>'
#return f'<a href="#_footnote_{fnum}" id="_footnote_src_{fnum}"><sup>{fnum}</sup></a>'
start_mark = f"<{html_footnote_marker()}>"
end_mark = f"</{html_footnote_marker()}>"
footnote_rgx = re.compile(rf"\s*{start_mark}\s*(.*?)\s*{end_mark}", re.S)
result = footnote_rgx.sub(replace, text).rstrip() + "\n\n"
if footnotes:
result += '<hr class="footnote_rule"/>\n'
for n, footnote in enumerate(footnotes, 1):
result += f'<a href="#_footnote_src_{n}" id="_footnote_{n}"><sup>{n} </sup></a>{footnote}<br/>\n'
# Temporarily add room for footnotes to move to the top of the window for the test:
result += '<div style="height: 100lh"></div>\n'
return result