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);
}

View File

@@ -1,30 +1,20 @@
if __name__ == "__main__":
import sys
sys.path.append("../kutil")
sys.path.append("../target")
# if __name__ == "__main__":
# import sys
# sys.path.append("../kutil")
# sys.path.append("../target")
import sys
import re
import klammer_base
import kutil
import html_util
from html_util import E
import latex_util as L
import pprint
import phases
def escape_newlines(s):
# A newline becomes the marker the html paragraph pass turns back into a
# line break, so a verbatim source file keeps its lines. Used by Source.
return re.sub("\n", " ___NL___ ", s)
def undash(s):
# Verbatim text must show the hyphens the writer typed: the target's
# "--"/"---" transforms have already run, so put them back. Used by
# Code_fragment.
result = re.sub("__MDASH__", "---", s)
return re.sub("__NDASH__", "--", result)
def is_comment(s):
return s.strip().startswith("//")
@@ -168,9 +158,17 @@ def html_line(text):
tex_line -- a raw # in "#include" would start a text removal). The
html entities introduce no Klammertext special, so the two passes
cannot interfere. No target :escape entries apply here, so each
quoted special decodes back to its own character."""
return quote_specials(html_escape_rgx.sub(
lambda m: html_escapes[m.group()], text))
quoted special decodes back to its own character.
Last, the typographically active characters are hidden as KTESC
markers (klammer_base.hide_typographic): the html target's transforms
run over the final result, and without this a "--check" in a listing
became an en-dash. The markers decode after the transform pass.
tex_line needs no such step -- the tex target declares no typographic
transforms (LaTeX applies its input conventions itself), and its ~ is
this code's own markup for a preserved space."""
return klammer_base.hide_typographic(quote_specials(html_escape_rgx.sub(
lambda m: html_escapes[m.group()], text)))
def html_block(code, comment):
r"""One block: its lines beside its comment.
@@ -192,11 +190,25 @@ def html_block(code, comment):
return f'<div class="code_block">{result}</div>\n'
def expand_whitespace_markers(text, K=None):
def count(count_match):
count = int(count_match) if count_match else 1
return count
def replace_spaces(match):
return " " * count(match.group(1))
def replace_newlines(match):
return "\n" * count(match.group(1))
result = text
result = re.compile(r" *#- *", re.S).sub("", result)
result = re.compile(r" *#\+(\d*) *", re.S).sub(replace_spaces, result)
result = re.compile(r"\s*\#\/(\d*)\s*", re.S).sub(replace_newlines, result)
return result
class Code(klammer_base.Klammer_base):
id = 0
def __init__(self, K):
super().__init__(K)
self.text = phases.expand_whitespace_markers(self.text)
self.text = expand_whitespace_markers(self.text)
def annotated(self, pairs):
"""Does any block of this listing carry a comment?"""
@@ -332,7 +344,7 @@ class Code_fragment(klammer_base.Klammer_base):
# ordinary string and the machine did the escaping; html() got away with
# handling "<" by hand and tex() with nothing at all.
def html(self):
return f'<span class="code">{html_line(undash(self.code_text.strip()))}</span>'
return f'<span class="code">{html_line(self.code_text.strip())}</span>'
def tex(self):
return f"{{\\tt {tex_line(self.code_text.strip())}}}"
@@ -373,7 +385,7 @@ def extract_marked_region(src, marker, filename):
class Source(Code):
"""@source_listing -- a Code listing whose text comes from a FILE.
r"""@source_listing -- a Code listing whose text comes from a FILE.
It IS a Code: @source_listing and @code differ only in where the text
comes from, so they must render identically, and subclassing is what
@@ -406,7 +418,7 @@ class Source(Code):
except OSError as e:
raise Exception(
f'Cannot read the source listing "{self.filename}": {e.strerror}.\n'
f' A relative name resolves against the DOCUMENT\'s directory.')
f' A relative name resolves against the document\'s directory.')
if self.marker:
text = extract_marked_region(text, self.marker, self.filename)
self.text = text

View File

@@ -1,403 +0,0 @@
print("DEPRECATED")
if __name__ == "__main__":
import sys
sys.path.append("../kutil")
sys.path.append("../target")
import re
import klammer_base
import kutil
import html_util
from html_util import E
import latex_util as L
import pprint
import phases
def escape_newlines(s):
return re.sub("\n", " ___NL___ ", s)
# def literal_newline(s):
# def replace(match):
# before, after = match.groups()
# return f"{before}\\n{after}"
# backslash_pat = re.compile(r'(".*?)\n(.*?")', re.S)
# return backslash_pat.sub(replace, s)
def is_comment(s):
return s.strip().startswith("//")
def split_blocks(s):
print('-'*40)
print(s)
print('-'*40)
blocks = []
in_code = True
block = ""
for line in s.rstrip().split("\n"):
if is_comment(line):
if in_code:
blocks.append(block)
block = line + "\n"
in_code = False
else:
block += line + "\n"
else:
if not in_code:
blocks.append(block)
block = line + "\n"
in_code = True
else:
block += line + "\n"
if block:
blocks.append(block)
for block in blocks:
print("B:")
print(block)
return blocks
def parse_blocks(blocks):
box_comment_rgx = re.compile("\s*//(\d+)\s+.*", re.S)
i = 0
while i < len(blocks):
match = box_comment_rgx.match(blocks[i])
if match:
print(match.group(1))
i += 1
def get_blocks(s):
comment_pat = re.compile(r"(\s*)//(\d+)\s+(.*)", re.S)
blocks = []
lines = s.strip("\n").split("\n")
i = 0
uncommented = ""
while i < len(lines):
match = comment_pat.match(lines[i])
if match:
if uncommented:
blocks.append([uncommented.rstrip(), None])
uncommented = ""
count = int(match.group(2))
comment = match.group(3)
code = ""
j = 0
i += 1
while j < count:
line = re.sub("\n", "\\n", lines[i])
code += line + "\n"
j += 1
i += 1
blocks.append([code.strip("\n"), comment])
else:
uncommented += lines[i] + "\n"
i += 1
if uncommented:
blocks.append([uncommented.rstrip(), None])
return blocks
def latex_spaces(s):
def replace(match):
s = match.group(0)
if False and len(s) == 1:
return "~"
else:
result = "~" * len(s)
result = f"\\hphantom{{{result}}}"
return result
space_pat = re.compile(" +", re.S)
return space_pat.sub(replace, s)
def latex_unquote(s):
quoted = "asciicircum quotesingle asciigrave asciitilde asciitilde backslash".split()
quoted = [f"{{}}\text{e}{{}}" for e in quoted]
result = s
for q in quoted:
result = re.sub(q, "X", result)
result = re.sub(" ", "Y", result)
return result
def longest_line(s):
result = ""
for line in latex_unquote(s).split("\n"):
if len(line) > len(result):
result = line
return result
def literal_newline(s):
def replace(match):
before, after = match.groups()
return f"{before}\\n{after}"
backslash_pat = re.compile(r'(".*?)\n(.*?")', re.S)
return backslash_pat.sub(replace, s)
class Code(klammer_base.Klammer_base):
id = 0
def __init__(self, K):
super().__init__(K)
self.text = phases.expand_whitespace_markers(self.text)
def html(self):
if self.K_target == "html":
self.text = literal_newline(self.text)
# Escape Klammertext special characters so they survive
# re-insertion into the katom stream after @eval
self.text = self.text.replace("^", "^^")
self.text = self.text.replace("#", "^#")
self.text = self.text.replace("@", "^@")
self.text = self.text.replace("|", "^|")
self.blocks = get_blocks(self.text)
result = ''
for text, comment in self.blocks:
border = "code_border" if comment else "code_no_border"
body = E("div").body(text).cls(f"code_text {border}").str(None)
if comment:
body += "\n" + E("div").body(comment).cls("code_comment").str()
result += E("div").body(body).cls("code_block").str()
if self.number or self.caption:
#result = html_util.add_caption(
# result, "Listing", self.number, self.caption, "i", "left", "top")
caption = kutil.caption_marker("Listing", self.caption)
result = f'<div class="plain_caption code_caption">{caption}</div>{result}\n'
return result
@staticmethod
def escape_latex(s):
"""Escape LaTeX special characters in code text."""
# Backslash must be first (before adding more backslashes)
s = s.replace("\\", "\\textbackslash{}")
s = s.replace("{", "\\{")
s = s.replace("}", "\\}")
s = s.replace("%", "\\%")
s = s.replace("$", "\\$")
s = s.replace("&", "\\&")
s = s.replace("_", "\\_")
s = s.replace("^", "\\textasciicircum{}")
s = s.replace("~", "\\textasciitilde{}")
s = s.replace("<", "\\textless{}")
s = s.replace(">", "\\textgreater{}")
return s
def code_box(self, text):
kutil.msg(text)
result = ""
for line in text.rstrip().split("\n"):
indent = len(line) - len(line.lstrip())
eline = ("~" * indent) + line[indent:]
print(indent, line)
print(eline)
result += eline + "\\\\\n"
result = result[:-3]
print("RESULT:")
print(result)
return result
def tex(self):
parse_blocks(split_blocks(self.text))
return ""
# Escape Klammertext special characters
self.text = self.text.replace("^", "^^")
self.text = self.text.replace("#", "^#")
self.text = self.text.replace("@", "^@")
self.text = self.text.replace("|", "^|")
# Escape LaTeX special characters in code text
self.text = Code.escape_latex(self.text)
self.blocks = get_blocks(self.text)
strutvis = "0pt"
start_strut = f"\\rule[0pt]{{{strutvis}}}{{12pt}}"
end_strut = f"\\rule[-6pt]{{{strutvis}}}{{12pt}}"
caption_strut = f"\\rule[-8pt]{{{strutvis}}}{{6pt}}"
indent = "8pt"
comment_sep = "10pt"
i = 0
result = ""
count = len(self.blocks)
for text, comment in self.blocks:
print("TEXT:")
print(text)
print("COMMENT:")
print(comment)
text = " " + re.sub("\n", " \n ", text) + " "
longest = longest_line(text)
text = latex_spaces(text)
text = re.sub("\n", r"\\\\", text)
text = f"{start_strut}\\ttfamily {text}{end_strut}"
width = f"\\widthof{{\\ttfamily {longest}}}"
code = L.environment("minipage", text, width) + "\\\\\n"
print(code)
#code = "\\asymbox{" + self.code_box(text) + "}"
#print(code)
code = text
if comment:
#width = f"\\linewidth - {width} - {indent} - {comment_sep}"
"""
width = f"\\linewidth - \widestline - {indent} - {comment_sep}"
code = f"\\fcolorbox{{Gray}}{{LightGray}}{{{code}}}"
code += f"\\rule{{{comment_sep}}}{{{strutvis}}}" \
+ L.environment("minipage", "\\sffamily\\small\\raggedright " + comment, width)
"""
code = "\\asymbox{" + self.code_box(text) + "}" + comment
result += f"\\rule{{{indent}}}{{{strutvis}}}{code}"
if comment:
if i < count - 1 and self.blocks[i+1][1]:
result += "\\\\[4pt]"
i += 1
if not self.blocks[count-1][1]:
result = result[:-4]
if self.number or self.caption:
caption = kutil.caption_marker("Listing", self.caption)
if self.blocks[0][1]:
caption += caption_strut
strut = f"\\rule{{{indent}}}{{{strutvis}}}"
result = f"{strut}\\emph{{\\it {caption}}}\\newline\n" + result + "\n"
result = f"\\hypertarget{{Reference-Listing-{Code.id}}}{{}}\n{result}"
Code.id += 1
return result
def undash(s):
result = s
result = re.sub("__MDASH__", "---", result)
result = re.sub("__NDASH__", "--", result)
return result
class Code_fragment(klammer_base.Klammer_base):
def __init__(self, K):
super().__init__(K)
def html(self):
#print(f"code: |{self.code_text}|")
result = self.code_text.strip()
result = undash(result)
#result = re.escape(result)
result = re.sub("<", "&lt;", result)
result = re.sub(" ", "&nbsp;", result)
#print(f"code: |{self.code_text}| -> |{result}|")
return f'<span class="code">{result}</span>'
def tex(self):
return f"{{\\tt {self.code_text.strip()}}}"
def show(s):
print("-"*80)
print(s)
print("-"*80)
class Source(klammer_base.Klammer_base):
def __init__(self, K):
super().__init__(K)
with open(self.filename) as fp:
self.src = fp.read()
def tex(self):
result = self.src
# result = re.sub("#", "^#", result)
# result = re.sub("\\^", "\\^", result)
result = f"\\begin{{verbatim}}\n{result}\n\\end{{verbatim}}\n"
return result
def html(self):
result = escape_newlines(self.src.strip()) + "\n"
result = re.sub("@", "^@", result)
result = E("div").body(result).cls("code_text").str()
return result
# --------------------------------------------------------------------------------
if __name__ == "__main__":
s = """
int main(int argc, char* argv[])
{
//1 One line commented
int count = 12;
//3 Two lines commented
for (int i = 0; i < count; i++) {
std::cout << "Counter: " << i << "\n";
}
//1 A really long comment for one line. A really long comment for one line. A really long comment for one line.
std::cout << "End\n";
}
"""
get_blocks(s);
# class Code(klammer_base.Klammer_base):
# def __init__(self, K):
# super().__init__(K)
# #self.show("Code")
# if self.filename and self.text:
# raise Exception("Both :text and :filename cannot be defined")
# if K.filename:
# with open(K.filename) as fp:
# self.src = fp.read()
# if K.pattern:
# rgx = re.compile(f".*?({K.pattern}).*", re.S)
# match = rgx.match(self.src)
# if match is None:
# raise Exception(f"Match fails for @source_code: {K.pattern}")
# self.src = match.group(1)
# self.src = kutil.protect_klammertext_special_characters(self.src)
# else:
# self.src = self.text
# def html(self):
# #src = re.sub("\n", "<!-- -->", self.src) ?
# #result = f'<pre class="code">\n{self.src}\n</pre>\n'
# result = self.src
# result = undash(result)
# result = f'<pre>\n{result}\n</pre>\n'
# return result
# def tex(self):
# src = self.src
# src = re.sub(r"\\{", "{", src)
# src = re.sub(r"\\}", "}", src)
# result = f"\\begin{{lstlisting}}\n{src}\n\\end{{lstlisting}}\n"
# return result
# def txt(self):
# return "x~ " + self.src
# class Pathname(klammer_base.Klammer_base):
# def __init__(self, K):
# super().__init__(K)
# def html(self):
# return f'<span class="monospace">{self.s}</span>'
# def tex(self):
# result = self.s
# def replace(match):
# return '\\{}'.format(match.group(1))
# result = re.sub(r'\\', 'XXXBACKSLASHXXX', result)
# result = re.compile('\s*__UNSPACE__\s*', re.S).sub('', result)
# result = re.compile(r'([&${}%#_])').sub(replace, result)
# result = re.sub('\^', r'\\^{}', result)
# result = re.sub('~', r'\\~{}', result)
# result = re.sub(r'XXXBACKSLASHXXX', r'{\\textbackslash}', result)
# result = re.sub('\n', r'~\\\\\n', result.strip())
# result = re.sub(' ', '$~$', result)
# result = re.sub("'", r"{\\textquotesingle}", result)
# result = re.sub('"', r'{\\textquotedbl}', result)
# result = re.sub('--', '{-}{-}', result)
# result = r'{{\normalfont\texttt{{{}}}}}'.format(result.strip())
# result = re.sub(r'\{\\textbackslash\}\\#', '\\#', result)
# if self.small:
# result = '{{\\footnotesize{}}}'.format(result)
# return result
# def txt(self):
# return f"'{self.s}'"

View File

@@ -25,13 +25,3 @@ class Color(klammer_base.Klammer_base):
if self.text:
result = '{{{} {}}}'.format(result, self.text)
return result
def hex_color(name, hex):
def c(h):
return float(eval('0x' + h)) / 255.0
r = c(hex[:2])
g = c(hex[2:4])
b = c(hex[4:])
return '\\definecolor{{{}}}{{rgb}}{{{:.3f},{:.3f},{:.3f}}}'.format(
name, r, g, b)

View File

@@ -101,7 +101,7 @@ std::string tex_to_pdf(Machine& machine)
ss << " " << line << "\n";
}
ss << "Check log file: " << outbase << ".log";
throw Definition_error(ss.str(), Locator(), false);
throw Definition_error(ss.str(), Locator());
}
if (std::regex_search(xelatex_log, std::regex("Package rerunfilecheck Warning:"))) {
(void)K::log(1, "Rerunning xelatex because document structure has changed");
@@ -115,7 +115,7 @@ std::string tex_to_pdf(Machine& machine)
ss << " " << line << "\n";
}
ss << "Check log file: " << outbase << ".log";
throw Definition_error(ss.str(), Locator(), false);
throw Definition_error(ss.str(), Locator());
}
}
warn_wide_tables(xelatex_log);

View File

@@ -63,3 +63,20 @@
@@
@@document.html,tex :: @eval :cpp *KLAMMERTEXT_HOME*/sks/document/document document @ @@
# The following definition of @document.txt is not adequate (it ignores :files,
# for example), but it enables tests of other txt klammers for now. When one of
# the arguments is empty, the justifcation postprocess should remove multiple
# lines, but __VSPACE__ inserts a space character, which prevents the lines
# removal from the justification function. You can also see the extra space
# before the *subtitle* value.
@@document.txt ::
@eval "*title*".upper() @ @nl@
*subtitle*
*author* @nl@
*date*
*text*
@@

View File

@@ -16,9 +16,9 @@ bool strbool(const std::string& s, const Locator& loc)
std::vector<std::string> values = {"false", "False", "0", "true", "True", "1"};
if (is_not_in(s, values)) {
std::stringstream ss {};
ss << "The value \"" << s << "\" is not a Boolean values. Possible values are:\n"
ss << "The value \"" << s << "\" is not a Boolean value. Possible values are:\n "
<< join(values, ", ");
throw Argument_error(ss.str(), loc, false);
throw Argument_error(ss.str(), loc);
}
bool result = (find(values.begin(), values.end(), s) - values.begin()) > 2;
return result;
@@ -51,10 +51,15 @@ Document_class::Document_class(Machine& machine) : Klammer_base(machine)
// spaces, ~ expansion (resolve_filename_list in mac/file.cpp); the
// existence checks resolve relative names against the input directory,
// as parse_input_filename() will.
m_files = resolve_filename_list(get("files"), get("K_input_dir"));
// Filename-bearing values arrive ESCAPED for the target (the state
// stores escaped values so they flow correctly into output); a filename
// is programmatic use, so decode the markers first -- the C++ mirror of
// the Python-side unescape_ktesc() rule. Found 2026-08-22: under tex,
// ":files my_chapter" searched for "myKTESC005fKTESCchapter".
m_files = resolve_filename_list(ktesc_resolve(get("files")), get("K_input_dir"));
m_css_text = get("css_text");
m_css_filenames = resolve_filename_list(get("css_files"), get("K_input_dir"));
m_css_filenames = resolve_filename_list(ktesc_resolve(get("css_files")), get("K_input_dir"));
m_include_sks_css = strbool(get("include_sks_css"), loc);
frame_background_color = get("frame_background_color");
frame_text_color = get("frame_text_color");
@@ -62,7 +67,7 @@ Document_class::Document_class(Machine& machine) : Klammer_base(machine)
nav_text_color = get("nav_text_color");
js_text = get("js_text");
m_js_filenames = resolve_filename_list(get("js_files"), get("K_input_dir"));
m_js_filenames = resolve_filename_list(ktesc_resolve(get("js_files")), get("K_input_dir"));
m_include_sks_js = strbool(get("include_sks_js"), loc);
// font_dirs = word_split(get("font_dirs"));
@@ -138,33 +143,49 @@ void Document_class::save_string_input_as_file()
fs::path parse_input_filename(const std::string& s, const std::string& input_dir)
{
// The two-class rule (2026-08-22), replacing a three-stage search whose
// second stage could quietly shadow a file beside the document:
//
// * a BARE WORD -- no directory separator, no extension -- is the kt/
// SHORTCUT: ":files X" MEANS kt/X.kt in the root file's directory,
// and nothing else. Missing is an immediate error whose message
// teaches the convention, not a fallback. The kt/ directory is the
// conventional home for a document's input files, and putting the
// meaning entirely in the name lets several root documents share it.
//
// * anything else is a real PATHNAME, absolute or resolved against the
// input file's directory (K_input_dir), so a document renders
// identically wherever ktext is run from.
//
// Which file a name landed on is a DERIVED value, so "-v 1" reports it.
fs::path p(s);
bool bare = s.find('/') == std::string::npos && p.extension().empty();
if (bare) {
fs::path in_kt = fs::path(input_dir) / "kt" / (s + ".kt");
if (!file_exists(in_kt.string())) {
throw Argument_error(
"The \":files\" name \"" + s + "\" is a bare word, which by "
"convention means the file kt/" + s + ".kt in the root "
"document's directory:\n"
" " + in_kt.string() + "\n"
"That file does not exist. Create it there, or write a real "
"pathname (a name with a directory or the \".kt\" extension) "
"to use a file elsewhere.",
Locator::none());
}
// The resolved path itself shows kt/ -- no label needed.
(void)K::log(1, "Input file \"" + s + "\": " + in_kt.string());
return in_kt;
}
if (p.extension() != ".kt") {
p += ".kt";
}
if (p.is_absolute()) {
return p;
}
// A relative :files name resolves against the input file's directory
// (K_input_dir), so a document renders identically wherever ktext is
// run from; then the legacy kt/ subdirectory; a name found in neither
// is returned as given (cwd-relative) and errors downstream.
// Which of the three a name landed on is a DERIVED value -- the ".kt" may
// have been supplied, and the directory certainly was -- so "-v 1" reports
// it. A ":files chapter1" that quietly found kt/chapter1.kt rather than
// the file beside the document is exactly what the author cannot see.
fs::path in_input_dir = fs::path(input_dir) / p;
if (file_exists(in_input_dir.string())) {
(void)K::log(1, "Input file \"" + s + "\": " + in_input_dir.string());
return in_input_dir;
}
fs::path in_kt_dir = fs::path(input_dir) / "kt" / p;
if (file_exists(in_kt_dir.string())) {
(void)K::log(1, "Input file \"" + s + "\": " + in_kt_dir.string()
+ " (found in the kt/ subdirectory)");
return in_kt_dir;
}
return p;
(void)K::log(1, "Input file \"" + s + "\": " + in_input_dir.string());
return in_input_dir;
}
void Document_class::write(const std::string& filename, const std::string& contents)

View File

@@ -113,38 +113,14 @@ std::string Document_class::font_definitions()
ss << " --monospace: \"" << m_mono_font << "\", monospace;\n";
ss << "}\n";
}
// Emit scale factors so sans and mono fonts match the serif font.
// Three scaling methods (uncomment the desired one):
// x-height: serif_xh / other_xh (matches lowercase, like fontspec MatchLowercase)
// cap-height: serif_ch / other_ch (matches capitals)
// average: mean(serif_xh,serif_ch) / mean(other_xh,other_ch) (compromise)
float serif_xh = m_resolved_serif.xheight_ratio;
float serif_ch = m_resolved_serif.capheight_ratio;
float serif_avg = (serif_xh + serif_ch) / 2.0f;
if (serif_avg > 0.0f) {
auto scale = [&](const Resolved_font& other) -> std::string {
float other_avg = (other.xheight_ratio + other.capheight_ratio) / 2.0f;
if (other_avg > 0.0f && other_avg != serif_avg) {
char buf[16];
// float ratio = serif_xh / other.xheight_ratio; // x-height
// float ratio = serif_ch / other.capheight_ratio; // cap-height
float ratio = serif_avg / other_avg; // average
std::snprintf(buf, sizeof(buf), "%.4f", ratio);
return buf;
}
return "";
};
std::string sans_scale = scale(m_resolved_sans);
std::string mono_scale = scale(m_resolved_mono);
if (!sans_scale.empty() || !mono_scale.empty()) {
ss << ":root {\n";
if (!sans_scale.empty())
ss << " --sans-serif-scale: " << sans_scale << ";\n";
if (!mono_scale.empty())
ss << " --monospace-scale: " << mono_scale << ";\n";
ss << "}\n";
}
}
// Font-size normalization across families is no longer emitted from
// here (2026-08-22): font.css declares "font-size-adjust: ex-height 0.5"
// on body, and the browser renders every font at the same x-height --
// the same computation the former --sans-serif-scale/--monospace-scale
// factors did from build-time OS/2 metrics, but applied to EVERY family
// switch instead of the four CSS sites that remembered to multiply.
// The metric extraction in the font store remains (the tex path and
// kdesc --font still use it).
// Global font scale: applied to body font-size
if (m_font_scale != 1.0f) {
char buf[16];
@@ -368,7 +344,15 @@ Document_class::insert_section_numbers(const std::string& marker, bool add_to_to
{
std::vector<int> levels(9, 0);
std::smatch match {};
std::string pattern = R"(<([\w-]+)\s*(.*?)>(.*?)MARKER\s*</span>\s*(.*?)<.*)";
// The heading text (group 4) runs to the heading element's OWN closing
// tag (the \1 backreference), not to the first "<": a nested element in
// a section title -- @c's <span class="code">, an @i's <em> -- would
// otherwise end the capture early and silently truncate the title in
// BOTH tables of contents (article's single_page_toc and book's
// navigation TOC read the same capture). Found 2026-08-22 via a @c in
// an @s2 title; heading.cpp's section_rgx already used the
// close-on-own-tag idiom.
std::string pattern = R"(<([\w-]+)\s*(.*?)>(.*?)MARKER\s*</span>\s*(.*?)</\1>.*)";
pattern = string_replace(pattern, "MARKER", marker);
std::regex heading_rgx(pattern);
std::vector<std::pair<std::string, std::string>> modified_components {};

View File

@@ -38,104 +38,6 @@ std::string levels_to_section(std::vector<unsigned int> levels)
int part_number = 1;
int unnumbered_id = 0;
/*
std::tuple<std::string, std::vector<Heading>, std::vector<unsigned int>, unsigned int>
add_section_numbers(const std::string& s, const std::string& basename, std::vector<unsigned int> levels, unsigned int initial_id,
std::map<std::string, std::string>& section_id_map)
{
(void)K::log(3);
auto depth { levels.size() };
std::vector<Heading> headings {};
std::string text { s };
std::regex section_rgx (R"((.*?)<h(\d)(.*?)>(.*?)</h\2>)");
std::regex part_rgx (R"((.*?)<kt-part(.*?)>Part (\d+)\s*<br>\s*(.*?)\s*</kt-part>)");
std::regex id_rgx(R"((.*?)id=\"([-\w]+)\"(.*))");
unsigned int id_number { initial_id };
//std::string result {};
std::stringstream result {};
for (std::string line : regex_split(s, std::regex(R"(\n)"), false)) {
std::smatch match {};
if (std::regex_match(line, match, part_rgx)) {
std::string pre { match[1] };
std::string attr { match[2] };
std::string level { match[3] };
std::string title { match[4] };
std::string id_prefix { "_part_"};
std::string id {};
std::smatch id_match {};
if (std::regex_match(attr, id_match, id_rgx)) {
id = id_match[2];
} else {
id = id_prefix + std::to_string(part_number);
}
//title = "Part " + std::to_string(part_number) + " - " + title;
//std::string section = "Part " + level;
std::string section = "Part " + std::to_string(part_number);
elements_t part_title
{ html::elt("kt-part",
{ html::elt("span", section).attr("class", "sectionnumber"),
html::elt("span", trim(title)).attr("class", "sectiontitle") })
.attr("id", id)
.attr("data-level", level) };
result << pre << part_title << "\n";
headings.push_back(Heading(0, basename, section, id, "0", title));
part_number += 1;
} else if (std::regex_match(line, match, section_rgx)) {
std::string pre { match[1] };
int level { std::stoi(match[2]) };
std::string attr { match[3] };
std::string title { match[4] };
bool numbered = attr.find("numbered") != std::string::npos;
std::string section {};
if (numbered) {
levels[level-1] = levels[level-1] + 1;
for (unsigned int li = level; li < depth; li++)
levels[li] = 0;
section = levels_to_section(levels);
}
std::string id {};
std::smatch id_match {};
if (std::regex_match(attr, id_match, id_rgx)) {
id = id_match[2];
} else {
if (section.size() == 0) {
id = "_su_" + std::to_string(unnumbered_id++);
} else {
id = "_s_" + string_replace(section, ".", "_");
}
}
std::string link_target = file_basename(basename) + link_delimiter + id;
section_id_map[title] = link_target;
//<h1 id="image-tests" style="clear:both;" class="headerlink">
// <span class="sectionnumber">1</span>Image tests</h1>
elements_t title_parts {};
if (numbered) {
title_parts.push_back(html::elt("span", trim(section)).attr("class", "sectionnumber"));
}
title_parts.push_back(html::elt("span", trim(title)).attr("class", "sectiontitle"));
// msg() << "title_parts: " << title_parts << "\n";
elements_t section_title
{ html::elt("h"+std::to_string(level), title_parts)
.attr("id", id)
.attr("data-level", std::to_string(level))};
// msg() << pre << section_title << "\n";
result << pre << section_title << "\n";
headings.push_back(Heading(level, basename, section, id, "0", title));
} else {
result << line << "\n";
}
}
//std::cout << "add_section_numbers: " << result.str() << " "
//<< headings.size() << " " << levels.size() << " " << id_number << "\n";
return std::tuple(result.str(), headings, levels, id_number);
}
*/
std::string make_html_table_of_contents(std::vector<Heading> headings)
{
elements_t toc { html::elt("h1", "Contents") };

View File

@@ -4,18 +4,23 @@
--serif: Libre Baskerville, serif;
--sans-serif: Open Sans, sans-serif;
--monospace: Inconsolata, monospace;
--sans-serif-scale: 1;
--monospace-scale: 1;
}
body {
font-family: var(--serif);
font-size: 1rem;
/* One normalization for every family switch: render each font so its
x-height is 0.5em (the design's anchor -- EB Garamond comes UP ~19%,
Open Sans down ~7%). Inherited, so headings, code, footnotes and any
future rule are covered with no per-site scale factors; this replaced
the --sans-serif-scale/--monospace-scale system (build-time metrics,
applied at four sites and silently missed everywhere else). The tex
target's equivalent is fontspec's MatchLowercase. */
font-size-adjust: ex-height 0.5;
}
tt, .tt, pre {
font-family: var(--monospace);
font-size: calc(1em * var(--monospace-scale));
}
h1, h2, h3, h4, h5 {
@@ -23,16 +28,11 @@ h1, h2, h3, h4, h5 {
font-weight: normal;
}
h1 {
font-size: calc(1.2rem * var(--sans-serif-scale));
}
h2, h3, h4, h5 {
font-size: calc(1.1rem * var(--sans-serif-scale));
}
/* Heading SIZES live in sks/section/css/section.css (one ladder, one
place); this file assigns only the family. */
p {
line-height: 1.3;
line-height: 1.5;
}
.plain {
@@ -50,7 +50,6 @@ p {
.sansserif {
font-family: var(--sans-serif);
font-size: calc(1em * var(--sans-serif-scale));
}
.ritalic {

View File

@@ -33,7 +33,7 @@ def html_fontify(text, font_symbol, font_size):
result = text
if cls:
if font_symbol == "c":
result = re.sub(" ", "&nbsp;", result)
result = re.sub(" ", "&#160;", result)
cls = f'class="{cls}"'
sty = ""
if font_size != 1:

View File

@@ -1,7 +1,7 @@
@@page.html body :title :
<html>
<head>
<meta charset="utf-8">
<meta charset="utf-8"/>
<title>*title*</title>
<style>
body { font-family:Palatino,roman; margin:2em 5em; }

View File

@@ -13,15 +13,39 @@ def unescape_ktesc(s):
return result
return re.sub(r'KTESC([0-9a-f]+)KTESC', replace, s)
def escape_ktesc(s):
"""A KTESC marker for the characters of s -- the mirror of
Target::escape_marker() in mac/target.cpp (4 lowercase hex digits per
byte). The marker is inert through every re-read and through the
target's typographic transform pass, and decodes back to the characters
at final escape resolution (ktesc_resolve)."""
return 'KTESC' + ''.join(f'{ord(c):04x}' for c in s) + 'KTESC'
# The characters the SKS targets' typographic transforms act on: the
# hyphen runs (-- and ---), the quote conventions (` ' `` ''), and ~.
# Verbatim text (@c, @code, @source_listing) must reach the output exactly
# as written, so the code klammers hide each of these as a KTESC marker --
# a transform source can then never match -- and the markers decode after
# the transform pass has run.
# SYNC: the transform tables of the html and txt targets in
# sks/target/target.k. A transform built from a new character needs it
# added here, or verbatim text will show the transformed form.
TYPOGRAPHIC_CHARS = "-'`~"
def hide_typographic(s):
"""Hide the typographically active characters of s as KTESC markers so
the target's transform pass cannot change verbatim text."""
for c in TYPOGRAPHIC_CHARS:
s = s.replace(c, escape_ktesc(c))
return s
class Klammer_base:
def __init__(self, K):
#args = {k:escape(v) for k, v in K.__dict__.items()
args = {k:v for k, v in K.__dict__.items()
if not k.startswith('__')}
for key in args:
setattr(self, key, args[key])
#setattr(self, "_klammer_name", klammer_name)
#pprint.pprint(self.__dict__)

View File

@@ -21,10 +21,11 @@ def msg(text=""):
def escape(s):
result = s
# These were commented out -- has it been replaced?
# It's only used in kutil/klammer_base.py and block/block.py
# and does nothing now.
#result = re.sub(r"\b", r"\b", result)
#result = re.sub(r"\t", r"\t", result)
#result = re.sub("\f", r"\\f", result)
#result = re.sub("\v", r"\\v", result)
#result = re.compile("\\(.)").sub(r"\\\1", result)

View File

@@ -13,8 +13,8 @@ def html_link(target, link_text, is_section, no_quotation_marks):
link_text = target if no_quotation_marks else f"{target}"
else:
# Allow long URLs to break at a slash:
link_text = re.sub('/', '/<wbr>', target)
link_text = re.sub('/<wbr>/<wbr>', '//', link_text)
link_text = re.sub('/', '/<wbr/>', target)
link_text = re.sub('/<wbr/>/<wbr/>', '//', link_text)
link_text = f'<tt>{link_text}</tt>'
target = f"[{target}]" if is_section else target
#result = f"[__link__{target}__{link_text}__]"

View File

@@ -86,7 +86,7 @@ class Define(klammer_base.Klammer_base):
strut = r"\rule[-8pt]{0pt}{8pt}"
result = "\n\n".join([f"\\item[{{{fontify(term)}{strut}}}] {definition}"
for term,definition in self.items])
result = f"\\begin{{description}}[labelindent=12pt,nosep,itemsep=6pt,parsep=.5\parskip,style=nextline]\n{result}\n\\end{{description}}"
result = f"\\begin{{description}}[labelindent=12pt,nosep,itemsep=6pt,parsep=.5\\parskip,style=nextline]\n{result}\n\\end{{description}}"
return result
# Needs to be implemented:
@@ -99,11 +99,11 @@ class Lines(klammer_base.Klammer_base):
super().__init__(K)
def html(self):
#return " <br>".join(self.s.strip().split("\n"))
#return " <br/>".join(self.s.strip().split("\n"))
#return f'<pre style="font-family: var(--serif)">{self.s}</pre>'
#return f'<pre>{self.s}</pre>'
result = self.s
result = re.sub("\n", "<br>\n", result)
result = re.sub("\n", "<br/>\n", result)
result = re.sub(" ", "&^#160;", result)
return result
@@ -114,331 +114,6 @@ class Lines(klammer_base.Klammer_base):
return self.s.strip()
"""
def lines(K):
return {'html' : html_lines,
'tex' : tex_lines,
'pdf' : tex_lines}[K._target](K)
def tex_lines(K):
return " \\\\\n".join(K.s.strip().split("\n"))
def html_lines(K):
return " <br>".join(K.s.strip().split("\n"))
"""
"""
def definition_item(K):
if K._format == 'html':
return '<dt>{}</dt>\n<dd>{}</dd>'.format(
format_for_paragraphs(K.term),
format_for_paragraphs(K.desc))
else:
leading = '' if K.inlist else '\\vspace*{-16pt}'
return '\\item[{}]\\leavevmode{}\n\n{}'.format(
K.term, leading, K.desc)
"""
"""
def list(K, list_type):
items = kutil.rest_args(K.list_items)
return {'html' : html_list,
'tex' : tex_list,
'pdf' : tex_list}[K._target](K, list_type, items)
def html_list(K, list_type, items):
body = "\n" + "".join([E("li").body(e).str()+"\n" for e in items])
result = E(list_type).body(body).str().rstrip()
return result
def tex_list(K, list_type, items):
body = "\n\n".join([f"\\item {e}" for e in items])
command = {'ol' : 'enumerate', 'ul' : 'itemize'}[list_type]
topsep = '[topsep=0pt]'
listsep = '\\setlist{nolistsep}' if K.cmp else ''
result = f'{listsep}\n\\begin{{{command}}}{topsep}\n{body}\n\\end{{{command}}}\n'
return result
def describe(K):
items = "\n\n".join(kutil.rest_args(K.descriptions))
return {'html' : html_describe,
'tex' : tex_describe,
'pdf' : tex_describe}[K._target](K, items)
def html_describe(K, items):
return E("dl").body(items).str()
def tex_describe(K, items):
return f'\\begin{{description}}\n{items}\n\\end{{description}}'
def entry(K):
return {'html' : html_entry,
'tex' : tex_entry,
'pdf' : tex_entry}[K._target](K)
def html_entry(K):
#return f'<dt>{K.item}</dt>\n<dd>{K.description}</dd>'
return E("dt").body(K.item).str(0) + E("dd").body(K.description).str(0)
def tex_entry(K):
return f'\\item[{K.item}]\n{K.description}\n'
def lines(K):
return {'html' : html_lines,
'tex' : tex_lines,
'pdf' : tex_lines}[K._target](K)
def tex_lines(K):
return " \\\\\n".join(K.s.strip().split("\n"))
def html_lines(K):
return " <br>".join(K.s.strip().split("\n"))
"""
r"""
# List items
paragraph_separator_re = re.compile('\n *\n', re.S)
def format_for_paragraphs(s):
result = s
if len(paragraph_separator_re.findall(result)) > 0:
result = '\n\n{}\n\n'.format(result)
return result
def li(K):
if K._format == 'html':
result = K.s.strip()
result = format_for_paragraphs(result)
result = '<li>{}</li>'.format(result)
else:
#result = '\\item{{\\Kinlisttrue {}}} '.format(K.s.strip())
#result = '\\item\\begin\\Kinlisttrue {}\\end '.format(K.s.strip())
#result = '\\item\\parbox{{\\linewidth}}{{\\Kinlisttrue {}}} '.format(K.s.strip())
#result = '\\item\\Kinlisttrue {} '.format(K.s.strip())
result = '\\item {} '.format(K.s.strip())
if K.keep:
result = '\\parbox{{\\linewidth}}{{{}}}'.format(result)
return result
def mli(K):
result = '@0item {} | @0ulc\n'.format(K.args[0])
result += "\n".join(['@0li {} li@'.format(e) for e in K.args[1:]]) + '\n'
result += ' ulc@ item@\n'
return result
# Ordered and unordered lists:
def list_html(K, list_type):
result = ''
if K.list_items[0][0:4] != '<li>':
result = "\n".join(['<li>{}</li>'.format(e) for e in K.list_items])
else:
result = K.list_items[0]
tag = 'ol'
if list_type == 'ul':
tag = 'ul style="list-style-type:{};"'.format(K.bullet)
cls = 'compressed' if K.cmp else ''
if list_type == 'ul' and K.bullet == 'none':
cls += ' nobullet'
cls = ' class="{}"'.format(cls.strip())
result = '<{}{}>\n{}\n</{}>\n'.format(tag, cls, result, list_type)
return result
def list_latex(K, list_type):
result = ''
first_word = K.list_items[0][:5]
if first_word not in {'\\item', '\\parb'}:
result = " " + "\n".join(['@0li {} li@'.format(e.strip()) for e in K.list_items])
else:
result = K.list_items[0]
#options = 'nosep,itemsep=1pt,topsep=0pt,partopsep=8pt,parsep=4pt'
options = 'nosep,itemsep={}pt'.format(2 if K.cmp else 6)
if K.cmp:
itemsep=',nosep'
else:
itemsep=''
'''
if list_type == 'ul' and K.bullet == 'none' and K.indent:
left_margin = ',leftmargin=18pt'
else:
left_margin = ''
'''
left_margin = ''
if K.in_define:
if list_type == 'ul':
left_margin = ',leftmargin=4pt'
else:
left_margin = ',leftmargin=16pt'
if K.parsep:
parsep = K.parsep
else:
parsep = 2 if K.cmp else 6
#options = '[nosep,parsep={}pt{},{}]'.format(parsep, left_margin,itemsep)
options = '[parsep={}{}{}]'.format(parsep, left_margin, itemsep)
'''
if K.cmp:
options = '[nosep]'
else:
options = ''
'''
#options = '[itemsep={}]'.format(K.itemsep) if K.itemsep else ''
#options = '[topsep=0pt,parsep={}pt]'.format(2 if K.cmp else 6)
#options = '\\setlength{{\\topsep}{0pt}}\n\\setlength{{\parsep}}{{{}}}\n'.format(
bullet = ''
if list_type == 'ul' and K.bullet == 'none':
bullet = '\\renewcommand\\labelitemi{\\hspace*{-8pt}}'
itemsep = '\n\\setlength{{\\itemsep}}{{{}}}\n'.format(K.itemsep) if K.itemsep else ''
#result = '''\\listvmargin%
result = '''\\begin{{{0}}}{1}{2}
{3}
{4}
\\end{{{0}}}
'''.format('itemize' if list_type == 'ul' else 'enumerate',
options, itemsep, bullet, result.strip())
result = re.compile('\n\n', re.S).sub('\n', result)
result = re.compile(r' +\\begin', re.S).sub(r'\\begin', result)
if K.uncover:
N = 1
def replace(match):
nonlocal N
result = '{}<{}-> '.format(match.group(1), N)
N += 1
return result
result = re.compile(r'(\\item(\[|\s))').sub(replace, result)
return result
#\\listvmargin
def ul(K):
if K._format == 'html':
return list_html(K, 'ul')
elif K._format == 'latex':
return list_latex(K, 'ul')
else:
util.format_not_defined(K._format, 'ul')
def ol(K):
if K._format == 'html':
return list_html(K, 'ol')
elif K._format == 'latex':
return list_latex(K, 'ol')
else:
util.format_not_defined(K._format, 'ol')
# Definitions
def definition_item(K):
if K._format == 'html':
return '<dt>{}</dt>\n<dd>{}</dd>'.format(
format_for_paragraphs(K.term),
format_for_paragraphs(K.desc))
else:
leading = '' if K.inlist else '\\vspace*{-16pt}'
return '\\item[{}]\\leavevmode{}\n\n{}'.format(
K.term, leading, K.desc)
def dfont(K):
if K._format == 'latex':
result = {'r' : '\\normalfont',
'i' : '\\normalfont\\itshape',
'b' : '\\normalfont\\bfseries',
't' : '\\normalfont\\ttfamily',
's' : '\\normalfont\\sffamily\\large'
}[K.font]
elif K._format == 'html':
s = K.term
result = {'r' : s,
'i' : '<i>{}</i>'.format(s),
'b' : '<b>{}</b>'.format(s),
't' : '<span class="monospace">{}</span>'.format(s),
's' : '<span class="sansserif">{}</span>'.format(s)
}[K.font]
else:
error.Klammertext_error(
'There is not defined format named "{}".'.format(K._format))
return result
def escape_code_term(s):
def replace(match):
left, term, right = match.groups()
term = re.sub('\[', r'$\\lbrack$\\,', term)
term = re.sub('\]', r'\\,$\\rbrack$', term)
#term = re.sub('\[', '\\[', term)
#term = re.sub('\]', '\\]', term)
term = re.sub('--', '-{}-', term)
result = '{}{}{}'.format(left, term, right)
return result
pat = re.compile(r'(\\item\[)(.*?)(\]\\)', re.S)
return pat.sub(replace, s)
def define(K):
if K._format == 'latex':
#itemsep=1pt,topsep=2pt,partopsep=8pt,parsep=4pt,%
options = 'nosep,parsep=4pt,leftmargin=32pt'
fontname = {'r' : '\\normalfont',
'i' : '\\normalfont\\itshape',
'b' : '\\normalfont\\bfseries',
't' : '\\normalfont\\ttfamily',
's' : '\\normalfont\\sffamily',
'si' : '\\normalfont\\sffamily\\itshape',
'sb' : '\\normalfont\\sffamily\\bfseries',
'c' : '\\normalfont',
}[K.font]
term = K.s
if K.font == 'c':
term = escape_code_term(term)
#term = re.compile('item\[(.*?)\]', re.S).sub(r'item[\\verb+\1+]', term)
#term = re.compile('
return '''
\\listvmargin \\begin{{description}}[{},
labelindent=16pt,%
style=nextline,font={}]
%\\raggedright
{}
\\end{{description}} \\listvmargin\n'''.format(options,fontname, term)
elif K._format == 'html':
term_class = {'r' : 'normal',
'i' : 'italic',
'b' : 'bold',
't' : 'monospace',
's' : 'sansserifbody',
'si' : 'sansserifitalic',
'sb' : 'sansserifbold',
'c' : 'normal',
}[K.font]
term = K.s
if K.font == 'c':
#term = re.sub('&#8211;', '&#8208;'*2, term)
term = re.sub('&#8211;', '- __UNSPACE__ -', term)
body = re.sub('<dt>', '<dt class="{}">'.format(term_class), term)
return '''
<dl>
{}
</dl>'''.format(body)
"""
def make_column_lists(lst, count):
cols = math.ceil(len(lst) / count)

View File

@@ -8,7 +8,7 @@ kt-part {
padding-top: 1.5rem;
margin-top: 0px;
font-family: var(--sans-serif);
font-size: 1.5rem;
/* font-size: 2.03rem; */ /* ladder top: 1.125^6 ... */
}
kt-chapter {
@@ -16,10 +16,11 @@ kt-chapter {
padding-top: 1.5rem;
margin-top: 0px;
font-family: var(--sans-serif);
font-size: 1.5rem;
/* font-size: 1.80rem; */ /* ... 1.125^5 */
}
/*
h1 {
padding-top: 1.5rem;
margin-top: 0;
@@ -27,10 +28,58 @@ h1 {
h2, h3, h4, h5 {
padding-top: 0.5rem;
margin-bottom: .5rem;
padding-top: 0.25rem;
margin-bottom: .25rem;
padding-bottom: 0rem;
}
*/
/* The heading ladder: a modular scale, ratio 1.125 (a "major second"),
flattening to weight below h4 -- seven visibly distinct sizes is more
hierarchy than an eye tracks. font-size-adjust (font.css) makes these
steps family-independent, so the rem values are the whole truth. */
/*
h1 {
font-size: 1.60rem;
}
h2 {
font-size: 1.42rem;
}
h3 {
font-size: 1.27rem;
}
h4 {
font-size: 1.13rem;
}
h5, h6 {
font-size: 1rem;
font-weight: bold;
}
*/
/* Too big. How about: */
kt-part { font-size: 1.4rem; }
kt-chapter { font-size: 1.4rem; }
h1 { font-size: 1.3rem; }
h2 { font-size: 1.2rem; }
h3 { font-size: 1.15rem; }
h4, h5, h6 { font-size: 1.125rem; }
/* Vertical space not used to indicate hierarchical level: */
kt-part, kt-chapter, h1, h2, h3, h4, h5, h6 {
margin-top: 1.2rem;
margin-bottom: 0.0rem;
padding-top: 0;
padding-bottom: 0;
}
/* Table of contents: */
.sectiontitle {
}

View File

@@ -12,6 +12,9 @@
@@s1.k title :id :n : Top level division, numbered @@
@@s1.html,tex :: @eval section.Section(K, 1) eval@ @@
# Temporary hack for testing txt target; need to number section headings.
@@s1.txt :: *title* @@
@@s2.k title :id :n : Second level division, numbered @@
@@s2.html,tex :: @eval section.Section(K, 2) eval@ @@

View File

@@ -15,11 +15,6 @@
vertical-align: middle;
}
.center {
display: flex;
justify-content: center;
}
/* -------------------------------------------------------------------------------- */
.bgap {
@@ -179,3 +174,20 @@
margin: 0 1em 0 1em;
}
*/
.footnote_in_text {
vertical-align: baseline;
position: relative;
top: -0.4em;
font-size: 70%;
line-height: 0;
padding-left: 1px;
font-weight: bold;
}
.footnote_rule {
width: 40%;
margin: 3lh 0 .5lh 0;
border: none;
border-top: 1px solid black;
}

View File

@@ -8,7 +8,7 @@ import kutil
_novalue = '__no_value__'
class E:
void_elements = set("area base br col embed hr img input link meta param source track wbr".split())
void_elements = "area base br col embed hr img input link meta source track wbr".split()
def __init__(self, tag):
self._tag = tag
self._sty = []
@@ -17,6 +17,7 @@ class E:
self._attr = []
self.no_value = '__novalue__'
def sty(self, name, value=_novalue):
if name and value is _novalue:
self._sty.append(name.strip(';'))
@@ -49,6 +50,7 @@ class E:
self._attr.append(attr)
return self
def __str__(self):
def label(name, s):
return ' {}="{}"'.format(name, s) if s else ''
@@ -60,7 +62,16 @@ class E:
b = "\n".join(self._body)
tag = '{}{}{}{}'.format(self._tag, a, c, s).strip()
end_tag = '</{}>'.format(self._tag) if self._tag not in E.void_elements else ""
return '<{}>{}{}\n'.format(tag, b, end_tag)
no_body = self._tag in E.void_elements
if b and no_body:
raise Exception(f"Tag {self._tag} incorrectly defines a body.")
if no_body:
result = f'<{tag}/>'
else:
result = f'<{tag}>{b}{end_tag}\n' # .format(tag, b, end_tag)
if no_body:
print(result)
return result
def __repr__(self):
return self.__str__()
@@ -80,6 +91,9 @@ class E:
result = result.rstrip() + "\n"
return result
def color(rgb):
return "rgb(" + ",".join([f"{float(e)*100}%" for e in rgb.split(",")]) + ")"
def html_indent(filename):
command = "(progn (setq make-backup-files nil) (mark-whole-buffer) "
command += "(indent-region (point-min) (point-max) nil) (save-buffer))"
@@ -99,10 +113,10 @@ def page(head_elt, body, js_files=[], load_jquery=True):
def head(title, js_files=[], js_code="", css_files=[], css_code="",
include_fonts=True, google_font=[], favicon=None, load_jquery=True):
result = '<meta name="viewport" content="width=device-width, initial-scale=1">\n'
result += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">\n'
result = '<meta name="viewport" content="width=device-width, initial-scale=1"/>\n'
result += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>\n'
if favicon:
result += f'<link rel="icon" type="image/x-icon" href="{favicon}">\n'
result += f'<link rel="icon" type="image/x-icon" href="{favicon}"/>\n'
result += "".join([E("link").attr("href", e).attr("rel", "stylesheet").str() for e in css_files])
fonts = None
css = ''

View File

@@ -35,7 +35,7 @@ def block(material):
required to know that a target distinguishes horizontal from
vertical mode. Asserted by sks/tst/paragraph_test.sh.
"""
return "\\par\n" + material.strip("\n") + "\n\\par\n"
return "^'\\par'^\n" + material.strip("\n") + "\n^'\\par'^\n"
def environment(name, body, required=None, optional=None):
req = f"{{{required}}}" if required else ""
@@ -164,3 +164,35 @@ def add_caption(element, caption_label, number, caption_text, latex_width,
element = minipage(caption + "\\rule[-0.75\\baselineskip]{0pt}{0pt}\n" + element,
latex_width, vertical="t")
return caption_wrapper(element, hpos, offset=offset)
def define_color(name, rgb_or_hex):
if "," in rgb_or_hex:
r, g, b = [float(e) for e in rgb_or_hex.split(",")]
else:
def c(h):
return float(eval('0x' + h)) / 255.0
r = c(rgb_or_hex[:2])
g = c(rgb_or_hex[2:4])
b = c(rgb_or_hex[4:])
return r'\\definecolor{{{}}}{{rgb}}{{{:.3f},{:.3f},{:.3f}}}'.format(name, r, g, b)
COLOR_BOX_TEMPLATE = r'''
^'\hfill\begingroup
COLOR
\setlength{\fboxsep}{8pt}
\fcolorbox{bordercolor}{localcolor}{
\parbox{WIDTH\linewidth}{\raggedright\setlength{\parskip}{8pt}'^
TEXT
^'}}\endgroup'^
'''.strip()
def color_box(text, width=1, box_color="1,1,1", border_color="0,0,0"):
color_definitions = define_color("localcolor", box_color) + "\n" \
+ define_color("bordercolor", border_color)
result = COLOR_BOX_TEMPLATE
for old, new in [['TEXT', re.sub(r'\\', r'\\\\', text)],
['COLOR', color_definitions],
['WIDTH', str(width)]]:
result = re.sub(old, new, result)
result = block(result)
return result

View File

@@ -1,331 +1,93 @@
import sys, os, re, textwrap, pprint
basedir = f'{os.environ["KLAMMERTEXT_HOME"]}/sks'
sys.path = [f"{basedir}/kutil"] + sys.path
sys.path = [f"{basedir}/target"] + sys.path
import kutil
import html_util
def tex_to_pdf(text):
print("in tex_to_pdf")
def expand_whitespace_markers(text, K=None):
def count(count_match):
count = int(count_match) if count_match else 1
return count
def replace_spaces(match):
return " " * count(match.group(1))
def replace_newlines(match):
return "\n" * count(match.group(1))
result = text
# result = re.sub(r"\s*#-\s*", "", result)
# space_pat = re.compile(r" *#\+(\d*) *", re.S)
# print("hits:", space_pat.findall(text))
# for p in space_pat.findall(text):
# print("FOUND:", p)
#
#result = re.sub(" ", "SPACE", result)
#result = re.sub("#/", "\n", result)
#result = re.sub("X", " ", result)
result = re.compile(r" *#- *", re.S).sub("", result)
result = re.compile(r" *#\+(\d*) *", re.S).sub(replace_spaces, result)
result = re.compile(r"\s*\#\/(\d*)\s*", re.S).sub(replace_newlines, result)
#print(result)
#sys.exit(0)
#print("expand_whitespace_markers", text, result)
return result
def handle_dashes(html_text, K=None):
def replace(match):
tag_start, text, tag_end = match.groups()
text = re.sub("__MDASH__", "---", text)
text = re.sub("__NDASH__", "--", text)
result = f"{tag_start}{text}{tag_end}"
return result
result = html_text
result = re.compile('(<span class="monospace">)(.*?)(</span>)', re.S).sub(replace, result)
result = re.sub("__MDASH__", "&#x2014;", result);
result = re.sub("__NDASH__", "&#x2013;", result);
return result
def add_tex_caption_numbers(tex_text, K=None):
chapter_pat = re.compile(r"\\section\\{")
index = {}
result = ""
chapter_number = 1
for line in tex_text.split("\n"):
if chapter_pat.search(line):
chapter_number += 1
reset_indices(index)
if kutil.caption_delimiter() in line:
before, caption_type, caption, after = line.split(kutil.caption_delimiter())
if index.get(caption_type) is None:
index[caption_type] = 1
n = index[caption_type]
number = f"{chapter_number}.{n}" if chapter_number != 0 else n
line = f"{before}{caption_type} {number} {caption}{after}"
index[caption_type] += 1
result += line + "\n"
return result
def remove_redundant_vspace(tex_text, K=None):
#print("remove_redundant_vspace")
rgx = re.compile(r"(\\vspace\*\{-[^}]+\})\s*\\vspace\*\{-[^}]+\}", re.S)
return rgx.sub(r"\1", tex_text)
def restore_backslash(tex_text, K=None):
#print("restore backslash")
#return re.compile(r"\^/").sub(r"\\", tex_text)
return re.sub("\6", "", tex_text)
#--------------------------------------------------------------------------------
def levels_to_section(levels):
result = ".".join([str(e) for e in levels])
result = re.sub(r"\.0", "", result)
return result
def chapter_title_span():
return '<span class="chapter_title">'
def add_html_section_numbers(html_text, K=None, start=0):
depth = 6
levels = [start-1] + ([0] * (depth-1))
section_pat = re.compile(r"(.*?)<h(\d)(.*?)>(.*?)</h\2>")
id_pat = re.compile(r'.*?id="([-\w]+)".*', re.S)
result = ""
id_number = 1
id_map = []
for line in html_text.split('\n'):
match = section_pat.match(line)
if match:
pre, level, attr, text = match.groups()
id_match = id_pat.fullmatch(attr)
if id_match:
id = id_match.group(1)
else:
id = f"id{id_number}"
attr = " " + f'id="{id}" {attr}'.strip()
id_number += 1
level = int(level)
levels[level-1] = levels[level-1] + 1
for i in range(level, depth):
levels[i] = 0
section = levels_to_section(levels)
line = f'{pre}<h{level}{attr}>{chapter_title_span()}{section}&#160;&#160;</span>{text}</h{level}>'
id_map.append([level, section, id, text])
result += line + "\n"
[print(e) for e in id_map]
return result, id_map
def make_html_table_of_contents(titles):
result = '<div id="_toc" class="toc-title">Table of contents</div>\n'
for level, section, id, text in titles:
result += f'<div class="level{level}"><a href="#{id}" class="level">{section} {text}</a></div>\n'
return result
def process_html_sections(html_text, K=None, start=1):
html, id_map = add_html_section_numbers(html_text)
toc = make_html_table_of_contents(id_map)
result = re.sub(r'(<div id="middle">)', rf"\1\n{toc}", html)
return result
def reset_indices(indices):
for key in indices.keys():
indices[key] = 1
def add_html_caption_numbers(html_text, K=None):
chapter_pat = re.compile(rf"{chapter_title_span()}(\d+)</span>")
index = {}
result = ""
chapter_number = ""
for line in html_text.split("\n"):
if '<span' in line:
match = chapter_pat.search(line)
print(match.groups())
if match and match.group(1) != "0":
chapter_number = f"{match.group(1)}."
reset_indices(index)
if kutil.caption_delimiter() in line:
before, caption_type, caption, after = line.split(kutil.caption_delimiter())
if index.get(caption_type) is None:
index[caption_type] = 1
n = index[caption_type]
line = f"{before}{caption_type} {chapter_number}{n} {caption}{after}"
index[caption_type] += 1
result += line + "\n"
return result
def escape_pre_angle_brackets(code, K=None):
def replace(match):
code = re.sub(r"<", "&#x003C;", match.group(1))
return f"<pre>{code}</pre>"
pat = re.compile("<pre>(.*?)</pre>", re.S)
return pat.sub(replace, code)
def block_elements():
return """
address article aside blockquote details dialog dd div dl dt fieldset
figcaption figure footer form h0 h1 h2 h3 h4 h5 h6 header hgroup hr li
main nav ol p section table ul td tr pre
""".strip().split()
def not_a_block(par):
if par.strip()[0] != "<":
return True
pat = re.compile(r'^</?([^\s>]+).*?>', re.S|re.M)
match = pat.match(par.strip())
return match.group(1) not in block_elements()
def make_paragraphs(html_text, K):
def replace(match):
before, content, after = match.groups()
result = ""
for par in [e.strip() for e in
re.compile(r"\n *(\n *)+", re.S).split(content)]:
if par.strip() and not_a_block(par):
#jpar = "\n".join(textwrap.wrap(par, width=80))
#result += f"\n<p>\n{jpar}\n</p>\n"
jpar = "\n".join(textwrap.wrap(f"<p>{par}</p>", width=80))
result += f"\n{jpar}\n"
else:
result += f"\n{par}\n"
result = re.compile(r"\s*</(\w+)>\n<\1>").sub(r"\n</\1>\n\n<\1>", result)
return f"{before}{result}{after}"
pat = re.compile(r'(.*?<div id="middle">)(.*?)(</div>\s*<div id="bottom">.*)', re.S)
match = pat.match(html_text)
result = pat.sub(replace, html_text)
result = re.compile(r"</td>\s+<td>", re.S).sub("</td>\n<td>", result)
result = re.compile(r"\n *(\n *)+", re.S).sub("\n\n", result)
return result
def indent_html(filename, K):
#print('indent_html')
html_util.html_indent(filename)
def copy_html_resources(filename, K):
output_dir = os.path.dirname(K._output_filename) or "."
css_dir = f"{output_dir}/{html_util.css_reldir()}"
kutil.make_dir_if_necessary(css_dir, delete_contents=True)
for basename, source in kutil.sks_files_of_type("css"):
command = f"cp {source} {css_dir}/{basename}.css"
os.system(command)
# def latex_escapes(latex_text):
# return latex_text
# result = latex_text
# result = re.sub('#', '\\#', result)
# result = re.sub('\^', '\\^', result)
# result += "LATEX"
# return result
# def restore_backslash(latex_text, K): # ?
# return latex_text
# print(latex_text)
# print("restore_backslash")
# result = latex_text
# result = re.sub('\b', r'\\b', result)
# result = re.sub(r'\\t', r'\\b', result)
# return result
def make_pdf_from_tex(filename, K, twice=True):
#debug_mode = int(K.K_verbose_level) > 1
verbose_level = int(K.K_verbose_level)
pathname = os.path.abspath(filename)
dirname, filename = os.path.split(pathname)
basename, ext = os.path.splitext(filename)
command = f"mv {pathname} {dirname}/{basename}.tex"
log_file = f"{dirname}/{basename}.log"
pdf_file = f"{dirname}/{basename}.pdf"
result = os.system(command)
#debug_mode = True
if verbose_level == 3:
remove_log = ''
else:
dbg_log = "/dev/null"
remove_log = ' >{} 2>&1'.format(dbg_log)
env_var = "KLAMMERTEXT_TEXLIVE_BIN"
texbin = os.environ.get(env_var)
if texbin is None:
msg = f"The directory of TeX Live commands must be defined by ${env_var}"
raise Exception(msg)
latex_command = f"{texbin}/xelatex"
#latex_command = f"{texbin}/pdflatex"
if not os.path.exists(latex_command):
raise Exception(f"LaTeX command not found: {latex_command}")
flags = "--halt-on-error"
#flags = "-file-line-error --shell-escape -halt-on-error -interaction nonstopmode -output-directory"
env = "export max_print_line=1000 ; export TEXINPUTS=${KLAMMERTEXT_HOME}/sks//: ;"
command = f"{env} cd {dirname} ; {latex_command} {flags} {basename}.tex {remove_log}"
#print(command)
result = os.system(command)
if result != 0:
os.system(f"tail -n 20 {log_file}")
msg = f"Error in LaTeX processing. See file {os.path.relpath(log_file)}"
#print(f"Error in LaTeX processing. See file {os.path.relpath(log_file)}")
raise Exception(msg)
# No error; is repetition necessary for links, etc? Check log for this.
# result = os.system(command)
# print(f"Wrote {os.path.relpath(pdf_file)}")
if twice:
result = os.system(command)
aux_files = 'aux out toc'.split()
if verbose_level < 2:
aux_files += 'tex log'.split()
for unused_ext in aux_files:
os.system('rm -rf {}/{}.{}'.format(dirname, basename, unused_ext))
import re
import textwrap
# 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.
vspace_marker = "__VSPACE__"
def justify_blocks(text, K=None):
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(vspace_marker)
if n and stripped == vspace_marker * n:
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
if par and par[0] not in {' ', '['}:
par = "\n".join(textwrap.wrap(par, width=80))
result += par + "\n\n"
return result.replace(vspace_marker, "\n")
if par and par[0] != ' ':
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 ""))
# if par and par[0] != ' ':
# par = "\n".join(textwrap.wrap(
# par, width=int(K.Target_txt_width), break_long_words=False))
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_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

View File

@@ -15,10 +15,30 @@ The LaTeX transformations supported by the SKS are:
'' -> close double quote
~ -> non-breaking space
"^" before any punctuation character quotes it (an engine rule, not an SKS
one): the character reaches the output literally, taking no part in the
transforms above -- ^-^- writes two plain hyphens (a --flag in prose), ^~ a
plain tilde. The one exception is the apostrophe: "^" before it opens a
literal span instead (a literal apostrophe is ^0027^), and writing that
pair even in THIS comment would open one -- the span sentinels are placed
before text removal. A target where the raw character would
still be re-interpreted declares its rendering with :resolve -- the tex
target maps a quoted hyphen to {-} (a bare -- in .tex re-forms TeX's dash
ligature) and a quoted tilde to \textasciitilde{} (a raw ~ is TeX's
non-breaking space). Verbatim text (@c, @code, ^'...'^) never needs any
of this: neither the transforms nor the quoting reach it.
]#
@@@target txt | Plain text with formatting | # This txt target is not full implemented in the SKS yet.
@@@state Target_txt_width
:desc Width of the text in the plain text target (txt)
:value 72
@@@
# The txt target is not fully implemented in the SKS yet.
@@@target txt | Plain text with formatting |
-- - |
--- -- |
`` " |
@@ -27,24 +47,35 @@ The LaTeX transformations supported by the SKS are:
' ' |
~ ^0020^
:after_apply
phases.justify_blocks
phases.txt_format_footnotes ;
phases.txt_justify_blocks
@@@
@@@target html | HTML page
# :escape < &lt; & &amp; |
|
--- &mdash; |
-- &ndash; |
--- &^#x2014; |
-- &^#x2013; |
`` “ |
'' ” |
` |
' |
^^~ &tilde; |
~ &nbsp;
~ &^#160;
:after_apply
phases.html_format_footnotes
@@@
# No transforms: LaTeX applies the input conventions itself. The :resolve
# entries say how a QUOTED character renders where the raw character would
# still be re-interpreted by TeX: ^- resolves to {-}, not a bare -, or the
# hyphens re-form a run and the dash ligature turns ^-^- into an en-dash;
# ^~ resolves to \textasciitilde{}, since a raw ~ is TeX's non-breaking
# space. Other quoted punctuation either has an :escape entry (its marker
# takes the escape replacement: ^$ renders as \$) or decodes to the raw
# character.
@@@target tex | LaTeX
:escape \ \textbackslash{} & \& { \{ } \} $ \$ % \% _ \_ ^# \^# ^^ \textasciicircum{}
:escape \ \textbackslash{} & \& { \{ } \} $ \$ % \% _ \_ ^# \^# ^^ \textasciicircum{}
:resolve - {-} ~ \textasciitilde{}
@@@
@@@target pdf | PDF from LaTeX

View File

@@ -570,8 +570,8 @@ def build_html(md_path, css_paths, fonts_css="", wrap=0, base=None, report=True)
css = fonts_css + "\n".join(Path(p).read_text(encoding="utf-8") for p in css_paths)
if base is None:
base = md_path.resolve().parent.as_uri() + "/"
return (f'<!doctype html>\n<html><head><meta charset="utf-8">\n'
f'<base href="{base}">\n'
return (f'<!doctype html>\n<html><head><meta charset="utf-8"/>\n'
f'<base href="{base}"/>\n'
f'<title>{md_path.stem}</title>\n'
f'<style>\n{css}\n</style>\n</head>\n<body>\n{body}\n</body></html>\n')