Files
klammertext/sks/target/html_util.py
Andy Kopra d61336b191 Escaping, table layout, and document fixes
Quoted Klammertext specials (^@ ^| ^# ^^ ^: ^*) and ^'...'^ regions now
survive re-processing (held as escape markers until final output);
:after_apply phase functions receive and return raw target text.

Tables: :hpos element position (center|left|right|<length>) replaces the
unimplemented :center/:indent; the ranged cell override is renamed
:justify; :column_width works in html (colgroup widths) and gains
'fill' -- the remaining width, capped at the column's widest entry, in
both targets; a table wider than the text column warns on the console;
table edges without an outer line set their text flush on the margins.

@document: no empty title bar for untitled documents; @vfill fills to
the bottom of the window in html (pure CSS); @vspace in plain text;
new @dot klammer; monospace email links.
2026-07-25 21:16:21 +02:00

317 lines
11 KiB
Python

import os
import re
import sys
import textwrap
sys.path.append(f'{os.environ.get("KLAMMERTEXT_HOME")}/sks/kutil')
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())
def __init__(self, tag):
self._tag = tag
self._sty = []
self._cls = []
self._body = []
self._attr = []
self.no_value = '__novalue__'
def sty(self, name, value=_novalue):
if name and value is _novalue:
self._sty.append(name.strip(';'))
elif value and value is not _novalue:
if type(value) is list:
value = " ".join(value)
self._sty.append('{}:{}'.format(name, value))
return self
def cls(self, c):
if c:
self._cls.append(c)
return self
def body(self, b, newline=True):
b = str(b)
#print("B:", b)
if newline and (b and b[0] == '<' or '\n' in b or len(b) > 72):
b = '\n' + b + '\n'
self._body.append(b)
return self
def attr(self, name, value):
self._attr.append('{}="{}"'.format(name, value))
return self
def data(self, name, value="true"):
attr = 'data-{}'.format(name)
attr = '{}="{}"'.format(attr, value)
self._attr.append(attr)
return self
def __str__(self):
def label(name, s):
return ' {}="{}"'.format(name, s) if s else ''
c = label('class', " ".join(self._cls))
s = "; ".join(self._sty).strip(' ;')
s = label('style', s + ';' if s else '')
a = " ".join(self._attr)
a = " " + a.strip() if a.strip() else ""
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)
def __repr__(self):
return self.__str__()
def __add__(self, e):
return str(self) + '\n' + str(e)
def str(self, newline=True):
result = str(self)
#print(f"begin: |{result}|")
if newline is None:
#result = result.strip("\n")
result = re.sub(">\n", ">", result)
#print("newline: None")
#print(f"str: |{result}|\n")
elif newline:
result = result.rstrip() + "\n"
return result
def html_indent(filename):
command = "(progn (setq make-backup-files nil) (mark-whole-buffer) "
command += "(indent-region (point-min) (point-max) nil) (save-buffer))"
os.system(f'emacs -nw -q --batch {filename} --eval "{command}" --kill 2> /dev/null')
def page(head_elt, body, js_files=[], load_jquery=True):
if True or js_files and load_jquery:
body += E("script").attr(
"src", "https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js").str()
for j in js_files:
body += E("script").attr("src", j).str()
#body_elt = E("body").attr("id", "body").body(f"\n\n{body}\n\n").str().strip()
body_elt = E("body").body(f"\n\n{body}\n\n").str().strip()
result = "<!DOCTYPE html>\n" + E("html").attr("lang", "en").body(head_elt + body_elt).str()
return result
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'
if favicon:
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 = ''
if not css_files and not css_code and include_fonts:
fonts, css = default_fonts()
fonts = google_font + fonts
if fonts:
result += E("link").attr("rel", "preconnect").attr("href", "https://fonts.gstatic.com").str()
#font = "|".join(google_font)
for font in fonts:
result += E("link").attr("href", f"https://fonts.googleapis.com/css?family={font}&display=swap") \
.attr("rel", "stylesheet").str()
#.attr("type", "text/css").str()
if css + css_code:
result += E("style").body(css + css_code).str()
if js_files and load_jquery:
result += E("script").attr(
"src", "https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js").str()
for j in js_files:
result += E("script").attr("src", j).str()
result += E("title").body(title).str().strip()
result = E("head").body(result).str()
return result
def default_fonts():
def font_name(spec):
return re.compile('[&|:]').split(re.sub(r'\+', ' ', spec))[0]
#serif = "PT+Serif:ital,wght@0,400;0,700;1,400"
#serif = "Manuale:ital,wght^@0,400;0,600;1,400"
serif = "Gentium+Book+Basic:ital,wght^@0,400;0,700;1,400"
monospace = "Roboto+Mono:ital,wght^@0,400;0,600;1,400"
sans = "PT+Sans:ital,wght^@0,400;0,700;1,400"
fonts = [serif, sans, monospace]
css = f'body {{ font-family: "{font_name(serif)}", serif; margin: 3rem; font-size: 14px; }}\n'
css += f'tt {{ font-family: "{font_name(monospace)}", sans-serif; font-size: 13px; }}\n'
css += f'.sans {{ font-family: "{font_name(sans)}", monospace; font-size: 14px; }}\n'
css += f'h1, h2, h3, h4 {{ font-family: "{font_name(sans)}", monospace; font-size: 20px; }}'
return fonts, css
def css_reldir():
return "css.ktdir"
"""
def css_basenames():
kdir = kutil.klammertext_dir()
result = []
for base, sks_dir in kutil.sks_dirs():
css_filename = f'{sks_dir}/{base}.css'
if os.path.exists(css_filename):
result.append(d)
return result
def css_source_files():
result = []
kdir = kutil.klammertext_dir()
for basename in css_basenames():
result.append([basename, f"{kdir}/sks/{basename}/{basename}.css"])
return result
"""
def css(styles, use_sks=False):
code = f'<style>\n{styles}\n</style>' if styles else ''
files = []
if use_sks:
for basename, filename in kutil.sks_files_of_type("css"):
files.append(f"{css_reldir()}/{basename}.css")
"""
kdir = kutil.klammertext_dir()
css_files = []
for basename in css_basenames():
files.append(f'{css_reldir()}/{basename}.css')
"""
return code, files
def element_tag(element):
tag_match = re.compile(r"<(\w+).*", re.S).match(str(element))
if not tag_match:
raise Exception(f"No tag found in {element}")
else:
return tag_match.group(1)
# 1. Relationship of element to caption
# 2. Relationship of element with/without caption to page
def font_class(font):
return {"r" : "", "i" : "ritalic", "t" : "monospace", "s" : "sanserif"}[font]
def hpos_container(element, hpos):
# Wrap element in its horizontal-position container. hpos is left,
# center, right, none, or a length, which becomes the left margin
# (the tex counterparts are the \LTleft glue for tables and the
# \hspace* in latex_util.caption_wrapper).
style = None
if hpos not in ("left", "center", "right", "none"):
length, _, _ = kutil.parse_length("html", hpos, 1)
style = f"margin-left: {length}"
hpos = "indent"
result = E("div").cls("hpos_" + hpos).body(element)
if style:
result.attr("style", style)
if hpos not in ("center", "indent"):
result.cls("hpos_margin")
return result
def add_caption(element, caption_label, number, caption_text,
font_symbol="i", hpos="center", side="bottom", as_string=True,
font_size=.9, width=None, max_width=None):
tag = element_tag(element)
# Caption
caption = ""
if number:
caption = kutil.caption_marker(caption_label, caption_text)
elif caption_text:
caption = caption_text
if caption:
# caption = kutil.protect_klammertext_special_characters(caption)
import font
caption = font.html_fontify(caption, font_symbol, font_size)
caption = E("div").cls("caption").body(caption, newline=False)
if caption_text and side == "bottom" or side == "top":
caption.cls("caption_text")
if caption:
if side == "left":
element.cls("lgap")
caption.cls("rgap")
elif side == "right":
element.cls("rgap")
caption.cls("lgap")
elif side == "bottom":
element.cls("bgap")
elif side == "top":
element.cls("tgap")
if side in {"left", "top"}:
element = str(caption) + "\n" + str(element)
else:
#element += "\n" + "\n".join(textwrap.wrap(str(caption), 80))
element += "\n" + re.sub("\n", " ", str(caption))
#print("-"*80)
#print(element)
#print("-"*80)
element = E("div").cls("caption_" + side).attr("data-label", caption_label).body(element)
if width:
# The element's width (e.g. a table's :column_width total) lives
# on this wrapper: the element fills it, and the caption tracks
# the element.
element.attr("style", f"width: {width}")
elif max_width:
# A content-sized element (a 'fill' table): the wrapper shrinks
# to it but never past the text column, so the element wraps at
# narrow windows instead of overflowing.
element.attr("style", f"max-width: {max_width}")
result = hpos_container(element, hpos)
if number:
result.cls("element_container")
if caption and hpos != "none":
if side == "bottom":
result.cls("mbot")
elif side == "top":
result.cls("mtop")
if as_string:
result = str(result) + "\n"
return result
if __name__ == '__main__':
elt = E("img").attr("src", "tree.jpg")
print(elt)
sys.exit(0)
import re
print(css("", True))
sys.exit(0)
def make_pars(s):
s = re.compile(r';(.*?)\.', re.S).sub(r';<tt>\1</tt>.', s)
s = re.sub('--', '&#8212;', s)
s = re.compile(r'"(\s)', re.S).sub(r'&#8221;\1', s)
s = re.compile('"', re.S).sub('&#8220;', s)
s = re.compile("'", re.S).sub('&#8217;', s)
s = re.sub("whale", "<i>whale</i>", s)
s = re.sub("Queequeg", "<b>Queegueg</b>", s)
s = re.compile(r"(CHAPTER.*?Rope)\.", re.S).sub(r'<span class="sans">\1</span>', s)
s = re.sub(" and ", ' <span class="sans"><b>and</b></span> ', s)
pars = re.compile('\n\n+').split(s)
result = "\n\n".join(["<p>{}</p>".format(e) for e in pars])
return result
google_font, css = default_fonts()
with open("../../../moby/moby-072.txt") as fp:
txt = make_pars(fp.read())
hd = head("Moby Dick - Chapter 72", css_code=css, google_font=google_font)
pg = page(hd, txt)
filename = "chapter_72.html"
with open(filename, "w") as fp:
fp.write(pg)
#html_indent(filename)