import re import textwrap import pprint import klammer_base import phases import kutil import latex_util import html_util from html_util import E import color class Indent(klammer_base.Klammer_base): """An indented block (@indent). :left and :right are widths: a bare number is characters in every target (2026-09-16); :right defaults to :left.""" def __init__(self, K): super().__init__(K) if not self.right: self.right = self.left def html(self): text_align = "justify" if not self.ragged else "left" left = kutil.parse_width("html", self.left)[0] right = kutil.parse_width("html", self.right)[0] return E("div").body(self.s)\ .sty("margin", f"0.5rem {right} 0.5rem {left}")\ .sty("text-align", text_align)\ .str() def tex(self): if self.ragged: self.s = "\\raggedright " + self.s left = kutil.parse_width("tex", self.left)[0] right = kutil.parse_width("tex", self.right)[0] result = rf"\hspace*{{{left}}}" + latex_util.minipage( self.s, rf"\linewidth - {left} - {right}", center=False) result = latex_util.block(result) return result def txt(self): left = int(kutil.parse_width("txt", self.left)[0]) right = int(kutil.parse_width("txt", self.right)[0]) indent = phases.txt_nbsp_marker() * left # a space that survives width = int(self.Target_txt_width) - left - right result = re.sub("KK0022", '"', self.s) return phases.txt_lay_out(indent + f"\n{indent}".join(phases.txt_wrap(result, width))) class Hspace(klammer_base.Klammer_base): """Horizontal space (@hspace): a width, so a bare number is characters in every target.""" def __init__(self, K): super().__init__(K) def html(self): return f'' def tex(self): return f"\\hspace{{{kutil.parse_width('tex', self.width)[0]}}}" def txt(self): # The nbsp marker, not spaces: an @eval result of only whitespace # is trimmed away entirely. return phases.txt_nbsp_marker() * int(kutil.parse_width("txt", self.width)[0]) class Note(klammer_base.Klammer_base): def __init__(self, K): super().__init__(K) def html(self): result = E("div").cls("box")\ .sty("width", f"{self.width*100}%")\ .sty("background-color", html_util.color(self.color))\ .sty("border-color", html_util.color(self.border_color))\ .sty("margin-left", "auto")\ .body(f"{self.label}: {self.s}")\ .str() return result def tex(self): return latex_util.color_box( f"\\textbf{{{self.label}:}} {self.s}", self.width, self.color, self.border_color) class Block(klammer_base.Klammer_base): def __init__(self, K): super().__init__(K) def tex(self): # NOT latex_util.block(): a textblock is absolutely positioned and # does not participate in the normal flow, so breaking the paragraph # around it would move the surrounding text. # # The trailing \par\kern0pt gives the block's vbox zero depth. # textpos anchors :point against the box HEIGHT alone, so content # whose last line has depth -- an @image, whose [c] minipage sits # half below the baseline -- was anchored a quarter of its height # too low (2in for a page-high picture) until 2026-09-08. to_x, to_y = [float(e) for e in self.to.split()] pt_x, pt_y = [float(e) for e in self.point.split()] result = f""" \\begin{{textblock}}{{{self.width}}}[{pt_x},{pt_y}]({to_x},{to_y}) \\setlength{{\\textwidth}}{{\\linewidth}} \\vspace*{{-1\\parskip}} {self.content.strip()} \\par\\kern0pt \\end{{textblock}} """ return result class Lines(klammer_base.Klammer_base): def __init__(self, K): super().__init__(K) self.lines = self.s.split("\n") def html(self): result = "" for line in self.lines: result += line + "
\n" return result def tex(self): return "\\\\\n".join(self.lines) + "\n" def txt(self): return self.lines class Vspace(klammer_base.Klammer_base): """Vertical space (@vspace): the first parameter of the height type. A bare number is lines in every target, as it always was here; a unit (2em, 12pt) is now accepted as well, under html and tex.""" def __init__(self, K): super().__init__(K) def html(self): height = kutil.parse_height("html", self.lines)[0] return f'
' def tex(self): return f"\\vspace{{{kutil.parse_height('tex', self.lines)[0]}}}" def txt(self): # One marker per line; the justification phase turns them into # blank lines after paragraph normalization (a whitespace-only # @eval result would be trimmed away, hence the markers). return phases.txt_vspace_marker() * int(kutil.parse_height("txt", self.lines)[0]) class Left_right(klammer_base.Klammer_base): def __init__(self, K): super().__init__(K) self.rows = list(zip(self.parts[0], self.parts[1])) def html(self): result = "" for left, right in self.rows: result += E("div").body( E("span").cls("left_right").body(left).str() + \ E("span").cls("left_right").sty("text-align", "right").body(right).str()).str(); result = E("p").body(result).str() return result def tex(self): result = "" for left, right in self.rows: result += rf"\parbox{{\linewidth}}{{{left} \hfill {right}}}" + " \\\\\n" result = result[:-3] return result def txt(self): # Plain text: the left text at the block indent, the right text ending # at Target_txt_width, the gap filled with the nbsp marker (the only # space that survives to the output, and what marks the block as laid # out). A pair that cannot share a line -- less than two columns # between them -- goes on two lines, the right text still flush right. nbsp = phases.txt_nbsp_marker() indent = int(self.Target_txt_indent) width = int(self.Target_txt_width) lines = [] for left, right in self.rows: left, right = left.strip(), right.strip() gap = width - indent - phases.txt_visible_length(left) - phases.txt_visible_length(right) if gap >= 2: lines.append(nbsp * indent + left + nbsp * gap + right) else: lines.append(nbsp * indent + left) lines.append(nbsp * max(indent, width - phases.txt_visible_length(right)) + right) return phases.txt_lay_out("\n".join(lines))