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.
317 lines
11 KiB
Python
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('--', '—', 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)
|