import re import klammer_base class Subsuper(klammer_base.Klammer_base): def __init__(self, K, position): super().__init__(K) self.position = position """ print("Subsuper:", self.args) parts = self.args.strip().split() if len(parts) != 2: raise Exception(f"Two arguments required, but argument is: {self.args.strip()}") self.base, self.script = parts """ def html(self): return f"{self.base}<{self.position}>{self.script}" def tex(self): if self.position == "sub": return f"${self.base}_{self.script}$" else: return f"${self.base}^{self.script}$" # The typeface-declaration marker. A klammer cannot declare a typeface # itself: in html the @font-face must be in the page's css and the files # localized into /fonts/, and in tex the \newfontfamily must be in # the preamble -- both are @document's, and @document runs AFTER the # klammers it contains (application is post-order). So @typeface leaves # this marker where the typeface is needed, and @document collects the # markers, resolves and localizes each typeface, declares it, and replaces # the marker with whatever inline switch the target needs (a font-switch # command for tex; for html the marker sits IN the span's font-family value # and becomes the family name the store resolved the typeface to, which # is what the localized css declares -- the name as WRITTEN may be the # store's directory name, "noto-sans-egyptian-hieroglyphs", and a span # carrying that matched no @font-face until 2026-09-12). Same shape as # the __CAPTION__ markers. # # SYNC: typeface_marker_rgx in sks/document/document_class.h -- a C++/Python # pair, so the constant cannot be single-sourced the way a .k/.py pair can # (TODO #47); change both or neither. def typeface_marker(name, scale): return f"__TYPEFACE__{name}__{scale}__" class Typeface(klammer_base.Klammer_base): """A temporary typeface change (@typeface). See sks/font/font.k.""" def __init__(self, K): super().__init__(K) def html(self): # font-size-adjust is switched OFF for the span: the document # normalizes families to a common x-height (font.css), which is # meaningless for a script whose glyphs have no x-height at all -- # hieroglyphs, cuneiform -- and would resize them unpredictably. # The family name is quoted -- an unquoted digit-initial word # ("Source Sans 3") is invalid CSS -- with SINGLE quotes, because # this sits inside a double-quoted html attribute. What is quoted # here is the MARKER: @document replaces it with the resolved # family name (see the marker note above). # The size is the scale times the body's own enlargement: the body # is rendered at "font-size-adjust: ex-height 0.5" (font.css), so # the visible text is larger than its nominal size, and a span # with the adjust off at a plain "Nem" sat about a fifth smaller # against it than the same :scale in pdf. @document emits the # factor as --typeface_scale (document_html.cpp, font_definitions); # the fallback 1 is for a fragment rendered without @document. # The "*" of calc() survives the @eval re-read (tested 2026-09-18); # the "--" of the property name is hidden below with the rest. style = "', serif; font-size-adjust: none" style += f"; font-size: calc({float(self.scale)}em * var(--typeface_scale, 1))" # :middle -- for a script with no baseline relationship to the # text. A browser sizes the span's inline box from the FONT's # ascender and descender, not the glyph's ink, and centres it on # the content area's midpoint; a hieroglyph font declares an # ascender of 1.0 em and no descender (NewGardinerOmni; Noto: # 1.324/-0.326), so at :scale 1.75 the box sat half an em too # high and every table row grew from 31 to 47 px (Noto: 62). # line-height 1 drops the body's leading from the enlarged box and # vertical-align middle centres it on the PARENT's x-height, which # is why this uses the text's metrics, not the script font's zeros. # Measured 2026-09-18: 40 px, the floor the ink allows; line-height # 0 alone gave 31 px with the glyphs overlapping the rows above. # Off by default: a Latin typeface must keep the shared baseline. if self.middle: style += "; line-height: 1; vertical-align: middle" if self.lift_length(): # Relative positioning moves the ink and nothing else: the # line box is laid out as if the span had not moved, which is # what a baseline correction wants (vertical-align would # re-lay the line). Negative top = up, as \raisebox in tex. style += f"; position: relative; top: -{self.lift_length()}" # The STYLE is markup, so its typographically active characters are # hidden: the html target's transforms turn a single quote into a # curly one, which broke the css font-family value. The MARKER is # not hidden: a hyphen in the typeface name would become a KTESC # sequence, which on the :files path is still there when @document # looks for the marker. The TEXT is writer content and is # deliberately left alone -- its quotes and dashes should be # transformed. hide = klammer_base.hide_typographic return (f'' + f'{self.text}') def tex(self): # The marker becomes the font-switch command; the group is what # makes the change temporary (the retired @font emitted a bare # \setmainfont, which changed the font for the rest of the # document). group = "{" + typeface_marker(self.name, self.scale) + " " + self.text + "}" if self.lift_length(): # A \raisebox produced HERE is safe from the generator escaping # pass: this klammer is applied after a generator's result has # been escaped, so its backslash is never swept -- the trap # @nl.tex fell into (2026-09-07) is a klammer applied inside a # generator's ARGUMENT, which @typeface shares with @b and @i. return "\\raisebox{" + self.lift_length() + "}" + group return group def lift_length(self): # :lift, validated to the units both targets can move by. The # length argtype admits w/h fractions and a quoted string, which # mean nothing as a vertical shift; a bare number would be columns # or lines and is refused for the same reason. lift = self.lift if not lift: return "" if not re.fullmatch(r"-?(\d+\.?\d*|\.\d+)(em|pt|px)", lift): raise Exception(f"@typeface :lift takes a length in em, pt or px, not '{lift}'") return lift def txt(self): # A typeface cannot be expressed in plain text. Declared in the # .k description, so this is documented behaviour rather than a # silent per-target divergence. return self.text def html_fontify(text, font_symbol, font_size): cls = {"r" : "", "i" :"ritalic", "b" : "bold", "s" : "sansserif", "t" : "monospace", "c" : "code"}[font_symbol] result = text if cls: if font_symbol == "c": result = re.sub(" ", " ", result) cls = f'class="{cls}"' sty = "" if font_size != 1: sty = f'style="font-size: {float(font_size) * 100}%"' attr = (cls + " " + sty).strip() if attr: result = f'{result}' return result def tex_fontify(text, font_symbol, font_size): command = {"r" : "\\rmfamily", "i" :"\\it", "b" : "\\bfseries", "s" : "\\sffamily", "t" : "\\ttfamily", "c" : "\\ttfamily"}[font_symbol] result = f"{{{command}\\scalefont{{{font_size}}}{{{text}}}}}" return result