305 lines
		
	
	
	
		
			9.7 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			305 lines
		
	
	
	
		
			9.7 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
from panflute import *
 | 
						||
from pygments import highlight
 | 
						||
from pygments.lexers import get_lexer_by_name
 | 
						||
from pygments.formatters import HtmlFormatter
 | 
						||
from pygments.util import ClassNotFound
 | 
						||
import os
 | 
						||
 | 
						||
from .whitespace import NBSP
 | 
						||
from .transform import FQuoted
 | 
						||
from .katex import KatexClient
 | 
						||
from .util import inlinify
 | 
						||
from .context import Group
 | 
						||
from .images import ImageProcessor
 | 
						||
 | 
						||
def html(e: Element, k: KatexClient, i: ImageProcessor, indent_level: int=0, indent_str: str="\t") -> str:
 | 
						||
	
 | 
						||
	# `only` attribute which makes transformed elements appear only in tex
 | 
						||
	# output or html output
 | 
						||
	if hasattr(e, "attributes") and "only" in e.attributes and e.attributes["only"] != "html":
 | 
						||
		return ""
 | 
						||
 | 
						||
	if isinstance(e, ListContainer):
 | 
						||
		return ''.join([html(child, k, i, indent_level, indent_str) for child in e])
 | 
						||
 | 
						||
	# Bits from which the final element output is built at the end of this
 | 
						||
	# function. Most elements override this by returning their own output.
 | 
						||
	tag = e.tag.lower()
 | 
						||
	attributes = ""
 | 
						||
	content_foot = ""
 | 
						||
	content_head = ""
 | 
						||
 | 
						||
	if isinstance(e, Str):
 | 
						||
		return e.text.replace(" ", " ")
 | 
						||
 | 
						||
	# Most elements fit the general template at the end of the function, just
 | 
						||
	# need their html tag specified.
 | 
						||
	tags = {
 | 
						||
		BulletList: "ul",
 | 
						||
		Doc: "main",
 | 
						||
		Emph: "em",
 | 
						||
		Caption: "figcaption",
 | 
						||
		Para: "p",
 | 
						||
		Header: "h"+str(e.level) if hasattr(e, "level") else "",
 | 
						||
		LineBlock: "p",
 | 
						||
		ListItem: "li",
 | 
						||
		SmallCaps: "span",
 | 
						||
		Strikeout: "strike",
 | 
						||
		Subscript: "sub",
 | 
						||
		Superscript: "sup",
 | 
						||
		Underline: "u",
 | 
						||
		TableBody: "tbody",
 | 
						||
		TableHead: "thead",
 | 
						||
		TableFoot: "tfoot",
 | 
						||
		TableRow: "tr",
 | 
						||
		TableCell: "td",
 | 
						||
	}
 | 
						||
	if type(e) in tags:
 | 
						||
		tag = tags[type(e)]
 | 
						||
 | 
						||
	# These are also disabled in pandoc so they shouldn't appear in the AST at all.
 | 
						||
	not_implemented = {
 | 
						||
		Citation: True,
 | 
						||
		Cite: True,
 | 
						||
		Definition: True,
 | 
						||
		DefinitionItem: True,
 | 
						||
		DefinitionList: True
 | 
						||
	}
 | 
						||
	if type(e) in not_implemented:
 | 
						||
		return f'<!-- FIXME: {type(e)}s not implemented -->'
 | 
						||
 | 
						||
	# Elements which can be represented by a simple string
 | 
						||
	simple_string = {
 | 
						||
		NBSP: " ",
 | 
						||
		Space: " ",
 | 
						||
		Null: "",
 | 
						||
		LineBreak: f"\n{indent_level*indent_str}<br>\n{indent_level*indent_str}",
 | 
						||
		SoftBreak: f" ",
 | 
						||
		HorizontalRule: f"{indent_level*indent_str}<hr>\n"
 | 
						||
	}
 | 
						||
	if type(e) in simple_string:
 | 
						||
		return simple_string[type(e)]
 | 
						||
 | 
						||
	if hasattr(e, "identifier") and e.identifier != "":
 | 
						||
		attributes += f' id="{e.identifier}"'
 | 
						||
 | 
						||
	if hasattr(e, "classes") and len(e.classes) != 0:
 | 
						||
		attributes += f' class="{" ".join(e.classes)}"'
 | 
						||
 | 
						||
	# Attributes are only passed down manually, because we use them internally.
 | 
						||
	# Maybe this should be a blocklist instead of an allowlist?
 | 
						||
 | 
						||
	# Overriding elements with their own returns
 | 
						||
	if isinstance(e, CodeBlock):
 | 
						||
		if len(e.classes) > 0 and (e.attributes["highlight"] == True or e.attributes["highlight"] == 'True'):
 | 
						||
			# Syntax highlighting using pygments
 | 
						||
			for cl in e.classes:
 | 
						||
				try:
 | 
						||
					lexer = get_lexer_by_name(cl)
 | 
						||
				except ClassNotFound:
 | 
						||
					continue
 | 
						||
				break
 | 
						||
			else:
 | 
						||
				print(f"WARN: Syntax highligher does not have lexer for element with these classes: {e.classes}")
 | 
						||
			formatter = HtmlFormatter(style=e.attributes["style"])
 | 
						||
			result = highlight(e.text, lexer, formatter)
 | 
						||
			return f'{result}'
 | 
						||
		else:
 | 
						||
			return f'<pre>{e.text}</pre>'
 | 
						||
 | 
						||
	if isinstance(e, Doc):
 | 
						||
		formatter = HtmlFormatter(style=e.get_metadata("highlight-style") if e.get_metadata("highlight-style") is not None else "default")
 | 
						||
		content_head = f'<style>{formatter.get_style_defs(".highlight")}</style>'
 | 
						||
 | 
						||
	if isinstance(e, Image):
 | 
						||
		url = e.url
 | 
						||
 | 
						||
		# Attributes → image processor args
 | 
						||
		additional_args = {}
 | 
						||
		if "file-width" in e.attributes:
 | 
						||
			additional_args["width"] = int(e.attributes["file-width"])
 | 
						||
		if "file-height" in e.attributes:
 | 
						||
			additional_args["height"] = int(e.attributes["file-height"])
 | 
						||
		if "file-quality" in e.attributes:
 | 
						||
			additional_args["quality"] = int(e.attributes["file-quality"])
 | 
						||
		if "file-dpi" in e.attributes:
 | 
						||
			additional_args["dpi"] = int(e.attributes["file-dpi"])
 | 
						||
 | 
						||
		# The directory of the current file, will also look for images there.
 | 
						||
		source_dir = e.attributes["source_dir"]
 | 
						||
 | 
						||
		_, ext = os.path.splitext(url)
 | 
						||
		ext = ext[1:]
 | 
						||
 | 
						||
		# Conversions between various formats.
 | 
						||
		if ext in ["svg", "png", "jpeg", "gif"]:
 | 
						||
			# Even supported elements have to be 'converted' because the
 | 
						||
			# processing contains finding and moving them to the output
 | 
						||
			# directory.
 | 
						||
			url = i.process_image(url, ext, source_dir, **additional_args)
 | 
						||
		elif ext in ["pdf", "epdf"]:
 | 
						||
			if not "dpi" in additional_args:
 | 
						||
				additional_args["dpi"] = 300
 | 
						||
			url = i.process_image(url, "png", source_dir, **additional_args)
 | 
						||
		elif ext in ["jpg"]:
 | 
						||
			url = i.process_image(url, "jpeg", source_dir, **additional_args)
 | 
						||
		else:
 | 
						||
			url = i.process_image(url, "png", source_dir, **additional_args)
 | 
						||
		
 | 
						||
		# Srcset generation - multiple alternative sizes of images browsers can
 | 
						||
		# choose from.
 | 
						||
		_, ext = os.path.splitext(url)
 | 
						||
		ext = ext[1:]
 | 
						||
		srcset = []
 | 
						||
		if ext in ["png", "jpeg"] and (not "no-srcset" in e.attributes or e.attributes["no-srcset"] == False or e.attributes["no-srcset"] == 'False'):
 | 
						||
			# This is inspired by @vojta001's blogPhoto shortcode he made for
 | 
						||
			# patek.cz:
 | 
						||
			# https://gitlab.com/patek-devs/patek.cz/-/blob/master/themes/patek/layouts/shortcodes/blogPhoto.html
 | 
						||
			width, height = i.get_image_size(url, [i.public_dir])
 | 
						||
			sizes = [(640, 360, 85), (1280, 720, 85), (1920, 1080, 90)] # (widht, height, quality)
 | 
						||
			for size in sizes:
 | 
						||
				if width <= size[0] and height <= size[1]:
 | 
						||
					srcset.append((f'{i.web_path}/{url}', f'{width}w'))
 | 
						||
					break
 | 
						||
				quality = size[2] if ext == "jpeg" else None
 | 
						||
				srcset.append((f'{i.web_path}/{i.process_image(url, ext, i.public_dir, width=size[0], height=size[1], quality=quality)}', f'{size[0]}w'))
 | 
						||
 | 
						||
		url = i.web_path + "/" + url
 | 
						||
		
 | 
						||
		attributes = f'{" style=width:"+e.attributes["width"] if "width" in e.attributes else ""} alt="{e.title or html(e.content, k, i, 0, "")}"'
 | 
						||
		if len(srcset) != 0:
 | 
						||
			return f'<a href="{url}"><img src="{srcset[-1][0]}" srcset="{", ".join([" ".join(src) for src in srcset])}"{attributes}></a>'
 | 
						||
		else:
 | 
						||
			return f'<img src="{url}"{attributes}>'
 | 
						||
 | 
						||
	# See https://pandoc.org/MANUAL.html#line-blocks
 | 
						||
	if isinstance(e, LineItem):
 | 
						||
		return indent_level*indent_str + html(e.content, k, i) + "<br>\n"
 | 
						||
 | 
						||
	# Footnotes are placed into parentheses. (And not footnotes (This is how KSP did it before me))
 | 
						||
	if isinstance(e, Note):
 | 
						||
		content_head = "("
 | 
						||
		content_foot = ")"
 | 
						||
		if inlinify(e) is not None:
 | 
						||
			return f' <note>({html(inlinify(e), k, i, 0, "")})</note>'
 | 
						||
 | 
						||
	if isinstance(e, FQuoted):
 | 
						||
		if e.style == "cs":
 | 
						||
			if e.quote_type == "SingleQuote":
 | 
						||
				return f'‚{html(e.content, k, i, 0, "")}‘'
 | 
						||
			elif e.quote_type == "DoubleQuote":
 | 
						||
				return f'„{html(e.content, k, i, 0, "")}“'
 | 
						||
		elif e.style == "en":
 | 
						||
			if e.quote_type == "SingleQuote":
 | 
						||
				return f'‘{html(e.content, k, i, 0, "")}’'
 | 
						||
			elif e.quote_type == "DoubleQuote":
 | 
						||
				return f'“{html(e.content, k, i, 0, "")}”'
 | 
						||
		else:
 | 
						||
			if e.quote_type == "SingleQuote":
 | 
						||
				return f'\'{html(e.content, k, i, 0, "")}\''
 | 
						||
			elif e.quote_type == "DoubleQuote":
 | 
						||
				return f'"{html(e.content, k, i, 0, "")}"'
 | 
						||
			else:
 | 
						||
				return f'"{html(e.content, k, i, 0, "")}"'
 | 
						||
 | 
						||
	if isinstance(e, Group):
 | 
						||
		k.begingroup()
 | 
						||
		ret = html(e.content, k, i, indent_level, indent_str)
 | 
						||
		k.endgroup()
 | 
						||
		return ret
 | 
						||
 | 
						||
	if isinstance(e, Math):
 | 
						||
		formats = {
 | 
						||
			"DisplayMath": True,
 | 
						||
			"InlineMath": False
 | 
						||
		}
 | 
						||
		return indent_level*indent_str + k.render(e.text, {"displayMode": formats[e.format]})
 | 
						||
 | 
						||
	if isinstance(e, RawInline):
 | 
						||
		if e.format == "html":
 | 
						||
			return e.text
 | 
						||
		else:
 | 
						||
			return ""
 | 
						||
 | 
						||
	if isinstance(e, RawBlock):
 | 
						||
		if e.format == "html":
 | 
						||
			return f'{e.text}\n'
 | 
						||
		else:
 | 
						||
			return ""
 | 
						||
 | 
						||
 | 
						||
	# Non-overriding elements, they get generated using the template at the end
 | 
						||
	# of this function
 | 
						||
	if isinstance(e, Header):
 | 
						||
		tag = "h"+str(e.level)
 | 
						||
 | 
						||
	if isinstance(e, Figure):
 | 
						||
		content_foot = html(e.caption, k, i, indent_level+1, indent_str)
 | 
						||
 | 
						||
	if isinstance(e, Caption):
 | 
						||
		tag = "figcaption"
 | 
						||
 | 
						||
	if isinstance(e, Link):
 | 
						||
		tag = "a"
 | 
						||
		attributes += f' href="{e.url}"'
 | 
						||
		if e.title:
 | 
						||
			attributes += f' title="{e.title}"'
 | 
						||
 | 
						||
	if isinstance(e, OrderedList):
 | 
						||
		tag = "ol"
 | 
						||
		if e.start and e.start != 1:
 | 
						||
			attributes += f' start="{e.start}"'
 | 
						||
		html_styles = {
 | 
						||
			"Decimal": "1",
 | 
						||
			"LowerRoman": "i",
 | 
						||
			"UpperRoman:": "I",
 | 
						||
			"LowerAlpha": "a",
 | 
						||
			"UpperAlpha": "A"
 | 
						||
		}
 | 
						||
		if e.style and e.style != "DefaultStyle":
 | 
						||
			attributes += f' type="{html_styles[e.style]}"'
 | 
						||
		# FIXME: Delimeter styles
 | 
						||
 | 
						||
	if isinstance(e, Table):
 | 
						||
		content_head = html(e.head, k, i, indent_level+1, indent_str)
 | 
						||
		content_foot = html(e.foot, k, i, indent_level+1, indent_str)
 | 
						||
		# FIXME: Fancy pandoc tables, using colspec
 | 
						||
 | 
						||
	if isinstance(e, TableCell):
 | 
						||
		tag = "td"
 | 
						||
		if e.colspan != 1:
 | 
						||
			attributes += f' colspan="{e.colspan}"'
 | 
						||
		if e.rowspan != 1:
 | 
						||
			attributes += f' rowspan="{e.rowspan}"'
 | 
						||
		aligns = {
 | 
						||
			"AlignLeft": "left",
 | 
						||
			"AlignRight": "right",
 | 
						||
			"AlignCenter": "center"
 | 
						||
		}
 | 
						||
		if e.alignment and e.alignment != "AlignDefault":
 | 
						||
			attributes += f' style="text-align: {aligns[e.alignment]}"'
 | 
						||
 | 
						||
	# The default which all non-overriding elements get generated by. This
 | 
						||
	# includes elements, which were not explicitly mentioned in this function,
 | 
						||
	# e. g. Strong
 | 
						||
 | 
						||
	if isinstance(e, Inline):
 | 
						||
		return f'<{tag}{attributes}>{content_head}{html(e.content, k, i, 0, "") if hasattr(e, "_content") else ""}{e.text if hasattr(e, "text") else ""}{content_foot}</{tag}>'
 | 
						||
 | 
						||
	out_str = ""
 | 
						||
	if not isinstance(e, Plain):
 | 
						||
		out_str += f"{indent_level*indent_str}<{tag}{attributes}>\n"
 | 
						||
	out_str += content_head
 | 
						||
	if hasattr(e, "_content"):
 | 
						||
		if len(e.content) > 0 and isinstance(e.content[0], Inline):
 | 
						||
			out_str += (indent_level+1)*indent_str
 | 
						||
		out_str += html(e.content, k, i, indent_level+1, indent_str)
 | 
						||
	if hasattr(e, "text"):
 | 
						||
		out_str += e.text
 | 
						||
	out_str += f"{content_foot}\n"
 | 
						||
	if not isinstance(e, Plain):
 | 
						||
		out_str += f"{indent_level*indent_str}</{tag}>\n"
 | 
						||
 | 
						||
	return out_str
 | 
						||
 | 
						||
 |