From be2d8ed723014e05287aa2d04ad0e9c46c3ccf72 Mon Sep 17 00:00:00 2001 From: Greenscreener Date: Fri, 3 Feb 2023 14:54:16 +0100 Subject: [PATCH] Finished KaTeX math rendering. --- formatitko.py | 6 ++---- html.py | 45 ++++++++++++++++++++++++++------------------- katex.py | 33 ++++++++++++++++++++++++++++++--- test.md | 10 +++++++++- 4 files changed, 67 insertions(+), 27 deletions(-) diff --git a/formatitko.py b/formatitko.py index 4293929..2bf4fe9 100755 --- a/formatitko.py +++ b/formatitko.py @@ -23,7 +23,5 @@ doc = doc.walk(transform, context) print("---------------------") #print(show(doc)) #print(convert_text(doc, input_format="panflute", output_format="markdown")) -#open("output.html", "w").write(" " + html(doc)) -k = KatexClient() -input() -print(k) +katexClient = KatexClient() +open("output.html", "w").write(" " + html(doc, katexClient)) diff --git a/html.py b/html.py index 207619d..b3f4f42 100644 --- a/html.py +++ b/html.py @@ -1,11 +1,12 @@ from panflute import * from whitespace import NBSP from transform import FQuoted +from katex import KatexClient -def html(e: Element, indent_level: int=0, indent_str: str="\t") -> str: +def html(e: Element, k: KatexClient, indent_level: int=0, indent_str: str="\t") -> str: if isinstance(e, ListContainer): - return ''.join([html(child, indent_level, indent_str) for child in e]) + return ''.join([html(child, k, indent_level, indent_str) for child in e]) tag = e.tag.lower() attributes = "" @@ -67,14 +68,14 @@ def html(e: Element, indent_level: int=0, indent_str: str="\t") -> str: tag = "pre" if isinstance(e, Figure): - content_foot = html(e.caption, indent_level+1, indent_str) + content_foot = html(e.caption, k, indent_level+1, indent_str) if isinstance(e, Caption): tag = "figcaption" if isinstance(e, Image): # TODO: Image processing - return f'{e.title or html(e.content, 0, ' + return f'{e.title or html(e.content, k, 0, ' if isinstance(e, Header): tag = "h"+str(e.level) @@ -86,13 +87,13 @@ def html(e: Element, indent_level: int=0, indent_str: str="\t") -> str: attributes += f' title="{e.title}"' if isinstance(e, LineItem): - return indent_level*indent_str + html(e.content) + "
\n" + return indent_level*indent_str + html(e.content, k) + "
\n" if isinstance(e, Note): content_head = "(" content_foot = ")" if len(e.content) == 1 and isinstance(e.content[0], Para): - return f' ({html(e.content[0].content, 0, "")})' + return f' ({html(e.content[0].content, k, 0, "")})' if isinstance(e, OrderedList): tag = "ol" @@ -110,8 +111,8 @@ def html(e: Element, indent_level: int=0, indent_str: str="\t") -> str: # FIXME: Delimeter styles if isinstance(e, Table): - content_head = html(e.head, indent_level+1, indent_str) - content_foot = html(e.foot, indent_level+1, indent_str) + content_head = html(e.head, k, indent_level+1, indent_str) + content_foot = html(e.foot, k, indent_level+1, indent_str) # FIXME: Fancy pandoc tables, using colspec if isinstance(e, TableCell): @@ -131,28 +132,34 @@ def html(e: Element, indent_level: int=0, indent_str: str="\t") -> str: if isinstance(e, FQuoted): if e.style == "cs": if e.quote_type == "SingleQuote": - return f'‚{html(e.content, 0, "")}‘' + return f'‚{html(e.content, k, 0, "")}‘' elif e.quote_type == "DoubleQuote": - return f'„{html(e.content, 0, "")}“' + return f'„{html(e.content, k, 0, "")}“' elif e.style == "en": if e.quote_type == "SingleQuote": - return f'‘{html(e.content, 0, "")}’' + return f'‘{html(e.content, k, 0, "")}’' elif e.quote_type == "DoubleQuote": - return f'“{html(e.content, 0, "")}”' + return f'“{html(e.content, k, 0, "")}”' else: if e.quote_type == "SingleQuote": - return f'\'{html(e.content, 0, "")}\'' + return f'\'{html(e.content, k, 0, "")}\'' elif e.quote_type == "DoubleQuote": - return f'"{html(e.content, 0, "")}"' + return f'"{html(e.content, k, 0, "")}"' else: - return f'"{html(e.content, 0, "")}"' + return f'"{html(e.content, k, 0, "")}"' if isinstance(e, Str): return e.text.replace(" ", " ") if isinstance(e, Math): - # TODO - return "TODO: MATH" + formats = { + "DisplayMath": True, + "InlineMath": False + } + # FIXME: Currently, all bits of math are isolated from each other, this + # means that \defs and and alike work only inside a single math block + # and are forgotten in the next one. + return indent_level*indent_str + k.render(e.text, {"displayMode": formats[e.format]}) if isinstance(e, RawInline) and e.format == "html": return e.text @@ -161,7 +168,7 @@ def html(e: Element, indent_level: int=0, indent_str: str="\t") -> str: return f'{e.text}\n' if isinstance(e, Inline): - return f"<{tag}{attributes}>{content_head}{html(e.content, 0, '')}{content_foot}" + return f"<{tag}{attributes}>{content_head}{html(e.content, k, 0, '')}{content_foot}" out_str = "" if not isinstance(e, Plain): @@ -170,7 +177,7 @@ def html(e: Element, indent_level: int=0, indent_str: str="\t") -> str: 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, indent_level+1, indent_str) + out_str += html(e.content, k, indent_level+1, indent_str) if hasattr(e, "text"): out_str += e.text out_str += f"{content_foot}\n" diff --git a/katex.py b/katex.py index dc54da9..e1ae1eb 100644 --- a/katex.py +++ b/katex.py @@ -1,12 +1,39 @@ import socket import subprocess import tempfile +import json +import os +from typing import Dict +class KatexError(Exception): + pass + class KatexClient: def __init__(self): self._client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self._temp_dir = tempfile.TemporaryDirectory(prefix='formatitko') - socket_file = self._temp_dir.name + "/katex-socket" - self._client.bind(socket_file) - self._server = subprocess.Popen(["node", "./katex-server/index.mjs", socket_file]) + self._socket_file = self._temp_dir.name + "/katex-socket" + self._server_process = subprocess.Popen(["node", "./katex-server/index.mjs", self._socket_file]) + while not os.path.exists(self._socket_file): + pass + while True: + try: + self._client.connect(self._socket_file) + except ConnectionRefusedError: + continue + break + + def render(self, tex: str, options: Dict={}): + self._client.sendall((json.dumps({"formulas":[{"tex":tex, "options": options}]})+"\n").encode("utf-8")) + data = self._client.recv(128) + while data[-1] != 0x0a: + data += self._client.recv(128) + response = json.loads(data) + if "error" in response: + raise Exception(response["error"]) + if "error" in response["results"][0]: + raise KatexError(response["results"][0]["error"]) + else: + return response["results"][0]["html"] + diff --git a/test.md b/test.md index 2fec3a2..84e2986 100644 --- a/test.md +++ b/test.md @@ -55,7 +55,15 @@ A non-breakable space bro A lot of spaces -A text with some $math$. +A text with some inline math: $\sum_{i=1}^nn^2$. Plus some display math: +$$ +\def\eqalign#1{\begin{align*}#1\end{align*}} +\eqalign{ + 2 x_2 + 6 x_3 &= 14 \cr + x_1 - 3 x_2 + 2 x_3 &= 5 \cr + -x_1 + 4 x_2 + \phantom{1} x_3 &= 2 +} +$$ This should be seen by all^[This is a footnote].