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)
143 lines
5.9 KiB
Python
143 lines
5.9 KiB
Python
import re
|
|
import textwrap
|
|
|
|
import kutil
|
|
|
|
# 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__'
|
|
text = re.sub("\n\n+", delim, text)
|
|
result = ""
|
|
for par in text.split(delim):
|
|
stripped = par.strip()
|
|
n = stripped.count(txt_vspace_marker())
|
|
if n and stripped == txt_vspace_marker() * n:
|
|
# 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
|