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:
@@ -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 #- __ @@
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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("<", "<", result)
|
||||
result = re.sub(" ", " ", 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}'"
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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*
|
||||
@@
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {};
|
||||
|
||||
@@ -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") };
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -33,7 +33,7 @@ def html_fontify(text, font_symbol, font_size):
|
||||
result = text
|
||||
if cls:
|
||||
if font_symbol == "c":
|
||||
result = re.sub(" ", " ", result)
|
||||
result = re.sub(" ", " ", result)
|
||||
cls = f'class="{cls}"'
|
||||
sty = ""
|
||||
if font_size != 1:
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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__)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}__]"
|
||||
|
||||
331
sks/list/list.py
331
sks/list/list.py
@@ -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('–', '‐'*2, term)
|
||||
term = re.sub('–', '- __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)
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
@@ -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@ @@
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 = ''
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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__", "—", result);
|
||||
result = re.sub("__NDASH__", "–", 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}  </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"<", "<", 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
|
||||
|
||||
|
||||
|
||||
@@ -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 < < & & |
|
||||
|
|
||||
--- — |
|
||||
-- – |
|
||||
--- &^#x2014; |
|
||||
-- &^#x2013; |
|
||||
`` “ |
|
||||
'' ” |
|
||||
` ‘ |
|
||||
' ’ |
|
||||
^^~ ˜ |
|
||||
~
|
||||
~ &^#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
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user