2026-08-23 20:48:26 +02:00
|
|
|
import re
|
|
|
|
|
import textwrap
|
2026-07-18 18:48:23 +02:00
|
|
|
|
2026-08-26 23:00:58 +02:00
|
|
|
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.
|
|
|
|
|
|
2026-08-23 20:48:26 +02:00
|
|
|
def txt_nl_marker():
|
|
|
|
|
return "__KTEXTNEWLINE__"
|
|
|
|
|
|
|
|
|
|
def txt_vspace_marker():
|
|
|
|
|
return "__KTEXTVSPACE__"
|
|
|
|
|
|
2026-09-07 22:37:05 +02:00
|
|
|
# A space that survives to the output. "~" used to serve, and could not:
|
|
|
|
|
# it is also a character a writer may want, and the substitution at the end
|
|
|
|
|
# of txt_justify_blocks turned EVERY tilde into a space -- so a literal
|
|
|
|
|
# tilde could not be written in the txt target at all (a "^~" resolves
|
|
|
|
|
# before the phases run, and by then is indistinguishable). A marker of
|
|
|
|
|
# its own frees the character. SYNC: the txt target's "~ ^0020^" transform
|
|
|
|
|
# in sks/target/target.k is the WRITER-facing convention (a tilde means a
|
|
|
|
|
# non-breaking space, as in LaTeX) and is deliberately unchanged.
|
|
|
|
|
def txt_nbsp_marker():
|
|
|
|
|
return "__KTEXTNBSP__"
|
|
|
|
|
|
|
|
|
|
# The tex line break, carried as a marker.
|
|
|
|
|
#
|
|
|
|
|
# @nl.tex used to emit "\\newline" directly, and that is destroyed the moment
|
|
|
|
|
# it passes through a GENERATOR. A klammer whose @eval result still holds
|
|
|
|
|
# klammers is generator output, so the machine escapes its text before
|
|
|
|
|
# applying them -- which is what lets a writer put a "%" in generated data
|
|
|
|
|
# without knowing LaTeX. A backslash meets the same rule and becomes
|
|
|
|
|
# "\\textbackslash{}". So a row written
|
|
|
|
|
#
|
|
|
|
|
# @offer_list work done @nl@ (delivery date) | 10 || ...
|
|
|
|
|
#
|
|
|
|
|
# resolved @nl to "\\newline" while binding the rest argument, handed that to
|
|
|
|
|
# Python as ordinary text, and had it neutralised on the way back:
|
|
|
|
|
# "\\textbackslash{}newline" reached the .tex and printed as literal text.
|
|
|
|
|
#
|
|
|
|
|
# The marker has no backslash, so the escape pass has nothing to act on, and
|
|
|
|
|
# tex_resolve_markers puts the real command back at the very end.
|
|
|
|
|
#
|
|
|
|
|
# LETTERS AND DIGITS ONLY -- deliberately unlike the txt markers above. The
|
|
|
|
|
# tex target escapes "_" to "\\_", so a "__KTEXT...__" marker would arrive as
|
|
|
|
|
# "\\_\\_KTEXT...\\_\\_" and never match. Nothing in this marker is in tex's
|
|
|
|
|
# :escape list.
|
|
|
|
|
def tex_nl_marker():
|
|
|
|
|
return "KTEXNEWLINEMARK"
|
|
|
|
|
|
|
|
|
|
# Declared as an :after_apply phase on BOTH tex and pdf. ":includes" carries
|
|
|
|
|
# escapes, transforms and resolves from tex into pdf but NOT phases
|
|
|
|
|
# (target_registry.cpp), and pdf's own phase compiles the document, so this
|
|
|
|
|
# has to run before it -- hence the explicit entry in each rather than
|
|
|
|
|
# inheritance.
|
|
|
|
|
def tex_resolve_markers(text, K):
|
|
|
|
|
return text.replace(tex_nl_marker(), "\\newline")
|
|
|
|
|
|
2026-08-23 20:48:26 +02:00
|
|
|
def txt_justify_blocks(text, K):
|
2026-07-18 18:48:23 +02:00
|
|
|
delim = '__DIVIDE__'
|
2026-07-25 21:16:21 +02:00
|
|
|
text = re.sub("\n\n+", delim, text)
|
2026-07-18 18:48:23 +02:00
|
|
|
result = ""
|
|
|
|
|
for par in text.split(delim):
|
2026-07-25 21:16:21 +02:00
|
|
|
stripped = par.strip()
|
2026-08-23 20:48:26 +02:00
|
|
|
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
|
2026-07-18 18:48:23 +02:00
|
|
|
|
2026-08-26 23:00:58 +02:00
|
|
|
# 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.
|
|
|
|
|
#
|
2026-09-07 22:37:05 +02:00
|
|
|
# The nbsp MARKER 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 marker is the only space that survives, and it
|
|
|
|
|
# becomes one at the end of this function. (It was the character
|
|
|
|
|
# "~" until 2026-08-27, which meant every tilde in the document
|
|
|
|
|
# became a space and a literal tilde could not be written at all.)
|
2026-08-26 23:00:58 +02:00
|
|
|
#
|
|
|
|
|
# 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.
|
2026-09-07 22:37:05 +02:00
|
|
|
nbsp = txt_nbsp_marker()
|
|
|
|
|
if par and par[0] != ' ' and not par.startswith(nbsp):
|
|
|
|
|
label = re.match("(?:" + re.escape(nbsp) + r")*\[\d+\] ", par)
|
2026-08-23 20:48:26 +02:00
|
|
|
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 ""))
|
2026-08-26 23:00:58 +02:00
|
|
|
result += par + "\n\n"
|
2026-08-23 20:48:26 +02:00
|
|
|
result = re.sub(" *" + txt_vspace_marker() + " *", "\n", result)
|
|
|
|
|
result = re.sub(r"\n? *" + txt_nl_marker() + r" *\n?", "\n", result)
|
2026-09-07 22:37:05 +02:00
|
|
|
result = result.replace(txt_nbsp_marker(), " ")
|
2026-08-23 20:48:26 +02:00
|
|
|
return result
|
|
|
|
|
|
2026-08-26 23:00:58 +02:00
|
|
|
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,
|
2026-09-07 22:37:05 +02:00
|
|
|
subsequent_indent=txt_nbsp_marker()*indent)
|
2026-08-26 23:00:58 +02:00
|
|
|
caption = "\n".join(caption)
|
|
|
|
|
caption_width = indented_width
|
|
|
|
|
line_length = max([len(e) for e in caption.split("\n")])
|
2026-09-07 22:37:05 +02:00
|
|
|
line = txt_nbsp_marker() * indent + "-" * line_length
|
2026-08-26 23:00:58 +02:00
|
|
|
return caption + "\n" + line
|
|
|
|
|
result = caption_rgx.sub(replace, text)
|
|
|
|
|
return result
|
|
|
|
|
|
2026-08-23 20:48:26 +02:00
|
|
|
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}]"
|
2026-09-07 22:37:05 +02:00
|
|
|
result += (txt_nbsp_marker() * (width - len(label))
|
|
|
|
|
+ label + " " + footnote + "\n\n")
|
2026-08-23 20:48:26 +02:00
|
|
|
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
|