2026-07-18 18:48:23 +02:00
|
|
|
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}</sub>"
|
|
|
|
|
|
|
|
|
|
def tex(self):
|
|
|
|
|
if self.position == "sub":
|
|
|
|
|
return f"${self.base}_{self.script}$"
|
|
|
|
|
else:
|
|
|
|
|
return f"${self.base}^{self.script}$"
|
|
|
|
|
|
2026-08-25 23:30:55 +02:00
|
|
|
# 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 <output>/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
|
2026-09-12 18:11:48 +02:00
|
|
|
# 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.
|
2026-08-25 23:30:55 +02:00
|
|
|
#
|
|
|
|
|
# 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
|
2026-09-12 18:11:48 +02:00
|
|
|
# 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).
|
2026-09-18 20:02:44 +02:00
|
|
|
# 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.
|
2026-09-12 18:11:48 +02:00
|
|
|
style = "', serif; font-size-adjust: none"
|
2026-09-18 20:02:44 +02:00
|
|
|
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()}"
|
2026-08-25 23:30:55 +02:00
|
|
|
# The STYLE is markup, so its typographically active characters are
|
|
|
|
|
# hidden: the html target's transforms turn a single quote into a
|
2026-09-12 18:11:48 +02:00
|
|
|
# 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'<span style="{hide("font-family: " + chr(39))}'
|
|
|
|
|
+ typeface_marker(self.name, self.scale) + hide(style) + '">'
|
2026-08-25 23:30:55 +02:00
|
|
|
+ f'{self.text}</span>')
|
|
|
|
|
|
|
|
|
|
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).
|
2026-09-18 20:02:44 +02:00
|
|
|
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
|
2026-08-25 23:30:55 +02:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
2026-07-18 18:48:23 +02:00
|
|
|
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":
|
2026-08-23 20:48:26 +02:00
|
|
|
result = re.sub(" ", " ", result)
|
2026-07-22 18:17:43 +02:00
|
|
|
cls = f'class="{cls}"'
|
2026-07-18 18:48:23 +02:00
|
|
|
sty = ""
|
|
|
|
|
if font_size != 1:
|
|
|
|
|
sty = f'style="font-size: {float(font_size) * 100}%"'
|
|
|
|
|
attr = (cls + " " + sty).strip()
|
|
|
|
|
if attr:
|
|
|
|
|
result = f'<span {attr}>{result}</span>'
|
|
|
|
|
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
|