Initial commit: Klammertext source distribution
Curated source subset assembled by klammertext-dev's doc/make_dist.sh: the Klammermachine (mac), the Standard Klammer Set (sks), the commands (com), editor plugins and install guides (doc), a test subset (tst), and lib/bin placeholders. Builds with 'make -C com'. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
290
sks/target/html_util.py
Normal file
290
sks/target/html_util.py
Normal file
@@ -0,0 +1,290 @@
|
||||
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 add_caption(element, caption_label, number, caption_text,
|
||||
font_symbol="i", hpos="center", side="bottom", as_string=True, font_size=.9):
|
||||
tag = element_tag(element)
|
||||
# Caption
|
||||
caption = ""
|
||||
if number == "true":
|
||||
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)
|
||||
|
||||
|
||||
result = element
|
||||
result = E("div").cls("hpos_" + hpos).body(element)
|
||||
if hpos != "center":
|
||||
result = result.cls("hpos_margin")
|
||||
|
||||
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('--', '—', s)
|
||||
s = re.compile(r'"(\s)', re.S).sub(r'”\1', s)
|
||||
s = re.compile('"', re.S).sub('“', s)
|
||||
s = re.compile("'", re.S).sub('’', 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)
|
||||
Reference in New Issue
Block a user