Verbatim-safe typography, ^-punctuation quoting, full-range ^UUUU^, polyglot html

Typographic transforms (---, quote pairs, ~) no longer touch verbatim
text: @c/@code/@source_listing content and ^'...'^ spans show exactly the
characters written. "^" before any punctuation character quotes it in
every target (the apostrophe excepted: ^' opens a literal span), with the
new :resolve option on @@@target declaring per-target renderings. The
^UUUU^ code-point form accepts 4-6 hex digits, the full Unicode range.
The html output and transform spellings are polyglot (XML-valid), in
preparation for an EPUB target. New suites: transform_test, character_test
(engine), typography_test (SKS).

(from dev 07ce5ea86a0a)
This commit is contained in:
2026-08-23 20:48:26 +02:00
parent d982c0d6cc
commit 37b6ba1c4f
77 changed files with 1665 additions and 1608 deletions

View File

@@ -9,49 +9,77 @@ paragraph pass recognizes as a block and leaves alone, and \par in
vertical mode is a no-op.
]#
@@par.k s : A paragraph of text @@
# Paragraph
@@par.k s : A paragraph of text. @@
@@par.html :: <p>*s*</p> @@
@@par.tex ::
\par
*s*
\par
@@
@@par.tex :: \par *s* \par @@
# In plain text a paragraph is delimited by blank lines, which
# phases.justify_blocks then fills; #/2 inserts them without depending on
# the definition body's own whitespace surviving extraction.
@@par.txt :: #/2*s*#/2 @@
# Non-breaking space
@@sp.k : Non-breaking space character @@
@@sp.html :: &^#160; @@
@@sp.tex :: ~ @@
# TODO: Easy in LaTeX; how to handle in HTML and plain text?
# @@footnote.k s : Footnote (TBD) @@
# @@footnote :: [*s*] @@
# Indented block
@@indent.k s :w.int 3 :linebreak.bool false : Indented block @@
@@indent.k s :left.int 8 :right.int -1 :ragged.bool false :
Indented block. The *left* and *right* values are the number of characters for
indentation (as roughtly defined by "ex"). By default, *right* is set to the
*left* value (with -1 as the sentinel).
@@
@@indent.html,tex,txt :: @eval block.Indent(K) eval@ @@
@@quote.k s :w.int 1 :source : Quotation block @@
@@quote.html ::
<div class="quote">
# Right justification
@@right.k s :margin 0 :top_margin 2 : Right-justified text @@
@@right.html ::
<div style="text-align: right; margin: *top_margin*ex *margin*ex 2ex 0;">*s*</div>
@@
@@right.tex ::
{\setlength{\topsep}{0pt}\setlength{\partopsep}{0pt}\setlength{\parskip}{*top_margin*ex}
\begin{flushright}
\raggedleft\rightskip=*margin*ex
*s*
</div>
\end{flushright}}
@@
@@quote.tex ::
\quoteblock{*s*}{*source*}
@@right.txt ::
@eval import textwrap; "\n".join([line.rjust(*Target_txt_width*, "~")
for line in textwrap.wrap("""*s*""", *Target_txt_width*)]) @
@@
@@quote.txt ::
@eval block.block_indent(K) eval@
@@
@@note.k s :label Note :color 1.0,1.0,0.9 :bordercolor 0.2,0.2,0.2 :level.int 0 :width
: Rectangular block for a special note @@
# Quote with attribution
@@note.html,tex :: @eval block.Note(K) eval@ @@
@@quote.k text | attribution : A block quote with attribution. @@
@@quote.html,tex ::
@indent *text* :left 8 @
@right *attribution* :margin 8
:top_margin @cond @eval "*K_target*" == "html" @ | -1 | 1 cond@ @
@@
# Highlighted note in text
@@note.k s :label Note :color 1.0,1.0,0.9 :border_color 0.2,0.2,0.2 :level.int 0 :width.float 0.5
: Rectangular block for an editorial note @@
@@note.html,tex :: @eval block.Note(K) @ @@
# Center
@@center.k s : Center text @@
@@ -62,42 +90,46 @@ vertical mode is a no-op.
@@
@@center.html ::
<p>
<div class="center">
*s*
</div>
</p>
@@
@@right.k s : Right-justified text @@
@@right.html ::
<b>TBD</b> *s*
@@
@@right.tex ::
\begin{flushright}
*s*
\end{flushright}
@@
# Newline
@@nl.k : Newline character @@
@@nl.html :: <br> @@
@@nl.html :: <br/> @@
@@nl.tex :: \newline @@
@@nl.txt :: \n @@
@@nl.txt :: @eval phases.txt_nl_marker() @ @@
# New page
@@newpage.k : Start new page @@
@@newpage.html :: @@
@@newpage.tex :: \newpage @@
@@newpage.txt :: @@
# Extend page
@@extendpage.k linecount : Extenad current page @@
@@extendpage.html :: @@
@@extendpage.tex :: \enlargethispage{*linecount*\baselineskip} @@
@@extendpage.txt :: @@
# Vertical space
@@vspace.k lines.float : Vertical space, in multiples of the current line height @@
@@vspace.tex :: \vspace{*lines*\baselineskip} @@
@@vspace.html :: <div style="height: *lines*lh"></div> @@
@@vspace.txt :: @eval "__VSPACE__" * round(*lines*) @ @@
@@vspace.txt :: @eval phases.txt_vspace_marker() * round(*lines*) @ @@
# Vertical fill to end of page
@@vfill.k :
Fill the vertical space so that any following text is flush with the bottom
@@ -112,6 +144,8 @@ only makes some vertical space. @@
@@vfill.txt :: @vspace 3 @ @@
# Questions and answers
@@qa.k question | answer : Question and answer formatting @@
@@qa.html,tex,txt ::
@b Q: @ *question*
@@ -119,6 +153,9 @@ only makes some vertical space. @@
@b A: @ *answer*
@@
# Absolute positioning
@@@argtype coords | x and y coordinates :pattern 'float'\s+'float' @@@
@@block.k : to.coords | content :width.float .5 :point.coords 0.0 0.0
@@ -126,18 +163,51 @@ only makes some vertical space. @@
@@block.tex :: @eval block.Block(K) eval@ @@
# Preserved line endings
@@lines.k s : Maintain line breaks @@
@@lines.html,tex,txt :: @eval block.Lines(K) eval@ @@
@@twocolumns.k s : Format *s* in two columns @@
@@twocolumns.tex ::
\begin{multicols}{2}
# Multiple columns of text
@@multicolumn.k s :n 2 :gap 3 : Format *s* in *n* columns (default: 2). The *gap* is in "ex" units. @@
@@multicolumn.tex ::
{\setlength{\columnsep}{*gap*ex}
\setlength{\multicolsep}{\parskip}
\begin{multicols}{*n*}
*s*
\end{multicols}
\end{multicols}}
@@
@@left_right.k left | right : Text left- and right- justified on one line @@
@@left_right.tex ::
\makebox[\dimexpr\linewidth+\labelindent\relax]{*left*\hfill *right*}
@@multicolumn.html ::
<p>
<div style="column-count: *n*; column-gap: *gap*ex; text-align: justify;">
*s*
</div>
</p>
@@
# Text justified left and right on one line
@@left_right.k parts.rest(2) :
Pairs of text left- and right- justified. First, left lines are written with a
bar (|) between each line. Then, two bars (||), and the right lines with a bar
betwen them.
@@
@@left_right.html,tex :: @eval block.Left_right(K) @ @@
# Footnote
@@footnote.k s : Footnote @@
@@footnote.html :: <kt-footnote>*s*</kt-footnote> @@
@@footnote.tex :: \unskip\footnote{*s*} @@
@@footnote.txt :: __ #- @eval phases.txt_footnote_marker() @ *s* @eval phases.txt_footnote_marker() @ #- __ @@
# This doesn't work, either, so it isn't the @eval:
# @@footnote.txt :: __ #- marker *s* marker #- __ @@

View File

@@ -5,72 +5,60 @@ import pprint
import klammer_base
import kutil
import latex_util
import html_util
from html_util import E
import color
class Indent(klammer_base.Klammer_base):
def __init__(self, K):
super().__init__(K)
self.s = kutil.escape(self.s)
# Doesn't do anything now:
# self.s = kutil.escape(self.s)
if self.right < 0:
self.right = self.left
self.reduce = self.left + self.right
def html(self):
return "FIX: INDENT " + self.s
text_align = "justify" if not self.ragged else "left"
return E("div").body(self.s)\
.sty("margin", f"0.5rem {self.right}ex 0.5rem {self.left}ex")\
.sty("text-align", text_align)\
.str()
def tex(self):
tab = f"\\hspace*{{{self.w}ex}}"
result = ""
if self.linebreak:
for e in self.s.split("\n"):
result += f"{tab}{e}\\\\\n"
result = result[:-3]
result = re.sub(r"\t", r"\\t", result)
else:
result = tab + latex_util.minipage(
"\\raggedright " + self.s, f"\\textwidth - {self.w}ex", center=False, vmargin="4pt")
#print(result)
return latex_util.block(result)
if self.ragged:
self.s = "\\raggedright " + self.s
result = rf"\hspace*{{{self.left}ex}}" + latex_util.minipage(
self.s, rf"\linewidth - {self.reduce}ex", center=False)
result = latex_util.block(result)
return result
def txt(self):
indent = " " * self.w
indent = "~" * self.left # Removed in phases.justify_blocks
width = int(self.Target_txt_width) - self.left - self.right + 1 # Off-by one for textwrap
result = self.s
result = re.sub("KK0022", '"', result)
result = indent + f"\n{indent}".join(textwrap.wrap(result, width=70, break_on_hyphens=False))
return f"@lit\n{result}\nlit@"
result = indent + f"\n{indent}".join(textwrap.wrap(result, width=width, break_on_hyphens=True))
return result
class Note(klammer_base.Klammer_base):
def __init__(self, K):
super().__init__(K)
def html(self):
color = ",".join([f"{float(e)*100}%" for e in self.color.split(",")])
result = f'''<div class="box" style="background-color: rgb({color}); border-color: rgb({self.bordercolor}) ;">
<b>{self.label}:</b> {self.s}
</div>'''
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"<b>{self.label}:</b> {self.s}")\
.str()
return result
def tex(self):
if self.width:
width = r'{}\\textwidth'.format(self.width)
else:
#width = r'\\textwidth - 16pt - {}\\leftmargin'.format(self.level)
width = r'\\linewidth - \\leftmargin + 2pt'
# The \par on each side comes from latex_util.block() below: an
# \fcolorbox is box material and must sit in vertical mode.
result = '''
\\begingroup
COLOR\\setlength{\\fboxsep}{8pt}
\\fcolorbox{bordercolor}{localcolor}{
\\parbox{WIDTH}{\\raggedright\\setlength{\\parskip}{8pt}
\\textbf{LABEL:} TEXT
}}\\endgroup
'''
result = re.sub('LABEL', self.label, result)
result = re.sub('TEXT', re.sub(r'\\', r'\\\\', self.s), result)
result = re.sub('COLOR', r'\\definecolor{{localcolor}}{{rgb}}{{{}}}\nCOLOR'.format(self.color), result)
result = re.sub('COLOR', r'\\definecolor{{bordercolor}}{{rgb}}{{{}}}\n'.format(self.bordercolor), result)
result = re.sub('WIDTH', width, result)
print(result)
return latex_util.block(result)
return latex_util.color_box(
f"\\textbf{{{self.label}:}} {self.s}",
self.width, self.color, self.border_color)
class Block(klammer_base.Klammer_base):
@@ -95,13 +83,12 @@ class Block(klammer_base.Klammer_base):
class Lines(klammer_base.Klammer_base):
def __init__(self, K):
super().__init__(K)
self.s = kutil.escape(self.s)
self.lines = self.s.split("\n")
def html(self):
result = ""
for line in self.lines:
result += line + "<br>\n"
result += line + "<br/>\n"
return result
def tex(self):
@@ -109,3 +96,24 @@ class Lines(klammer_base.Klammer_base):
def txt(self):
return self.lines
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

View File

@@ -1,6 +1,6 @@
p {
margin: .5rem 0 .5rem 0;
margin: 1ex 0;
}
.quote {
@@ -15,10 +15,8 @@ p {
overflow: auto;
}
.centered {
margin-left: auto;
margin-right: auto;
width: fit-content;
.center {
text-align: center;
}
.indent {
@@ -49,3 +47,8 @@ p {
.vfill {
flex-grow: 1;
}
.left_right {
display: inline-block;
width: calc((100vw - 60px) / 2);
}