Jiří Kalvoda
9 months ago
27 changed files with 578 additions and 1142 deletions
@ -1,3 +1,6 @@ |
|||||
[submodule "ucwmac"] |
[submodule "ucwmac"] |
||||
path = ucwmac |
path = ucwmac |
||||
url = git://git.ucw.cz/ucwmac.git |
url = git://git.ucw.cz/ucwmac.git |
||||
|
[submodule "src/formatitko/katex-server"] |
||||
|
path = src/formatitko/katex-server |
||||
|
url = https://gitea.ks.matfyz.cz:/KSP/formatitko-katex-server |
||||
|
@ -1,311 +0,0 @@ |
|||||
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 typing import Union |
|
||||
|
|
||||
from .whitespace import NBSP |
|
||||
from .elements import FQuoted |
|
||||
from .katex import KatexClient |
|
||||
from .util import inlinify |
|
||||
from .context import Group |
|
||||
from .images import ImageProcessor |
|
||||
|
|
||||
import warnings |
|
||||
warnings.warn("The html function has been deprecated, is left only for reference and will be removed in future commits. HTML_generator should be used in its place.", DeprecationWarning) |
|
||||
|
|
||||
def html(e: Union[Element, ListContainer], k: KatexClient, i: ImageProcessor, indent_level: int=0, indent_str: str="\t") -> str: |
|
||||
|
|
||||
warnings.warn("The html function has been deprecated, is left only for reference and will be removed in future commits. HTML_generator should be used in its place.", DeprecationWarning) |
|
||||
|
|
||||
# `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 isinstance(e, Header) 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 |
|
||||
|
|
||||
|
|
@ -0,0 +1 @@ |
|||||
|
Subproject commit 953b44e942282375ac369af233c123b28146713e |
@ -1 +0,0 @@ |
|||||
node_modules |
|
@ -1 +0,0 @@ |
|||||
This was made by Standa Lukeš @exyi |
|
@ -1 +0,0 @@ |
|||||
console.log(require('katex').renderToString('\\frac{2a}{b}')) |
|
@ -1,131 +0,0 @@ |
|||||
// KaTeX rendering server
|
|
||||
// Listens on unix socket, path is provided as first argument
|
|
||||
// Expects JSON lines, each line is a query with the following schema:
|
|
||||
// {
|
|
||||
// formulas: [
|
|
||||
// {
|
|
||||
// tex: string,
|
|
||||
// options?: object
|
|
||||
// }
|
|
||||
// ],
|
|
||||
// options?: object
|
|
||||
// }
|
|
||||
|
|
||||
// see https://katex.org/docs/options.html for list of available options
|
|
||||
// If options formulas[].options field is used, the global options field is ignored.
|
|
||||
|
|
||||
// For each line, returns one JSON line with the following schema:
|
|
||||
// {
|
|
||||
// results: [
|
|
||||
// { html?: string } | { error?: string }
|
|
||||
// ]
|
|
||||
// } | { error?: string }
|
|
||||
|
|
||||
|
|
||||
// If one formula is invalid, the error in results is used
|
|
||||
// If the entire query is invalid (couldn't parse JSON, for example), the outer error field is used
|
|
||||
|
|
||||
|
|
||||
import katex from 'katex' |
|
||||
import net from 'net' |
|
||||
import * as readline from 'readline' |
|
||||
|
|
||||
const myArgs = process.argv.slice(2) |
|
||||
|
|
||||
const unixSocketPath = myArgs[0] |
|
||||
if (!unixSocketPath) { |
|
||||
console.error('you must specify socket path') |
|
||||
process.exit(1) |
|
||||
} |
|
||||
|
|
||||
// This server listens on a Unix socket at /var/run/mysocket
|
|
||||
var unixServer = net.createServer(handleClient); |
|
||||
unixServer.listen(unixSocketPath); |
|
||||
console.log("OK") |
|
||||
|
|
||||
function handleExit(signal) { |
|
||||
// unixServer.emit('close')
|
|
||||
unixServer.close(function () { |
|
||||
|
|
||||
}); |
|
||||
process.exit(0); // put this into the callback to avoid closing open connections
|
|
||||
} |
|
||||
process.on('SIGINT', handleExit); |
|
||||
process.on('SIGQUIT', handleExit); |
|
||||
process.on('SIGTERM', handleExit); |
|
||||
process.on('exit', handleExit); |
|
||||
|
|
||||
const defaultOptions = {} |
|
||||
|
|
||||
/** |
|
||||
* @param {net.Socket} socket |
|
||||
* @returns {Promise<void>} |
|
||||
* */ |
|
||||
function socketWrite(socket, data) { |
|
||||
return new Promise((resolve, reject) => { |
|
||||
socket.write(data, (err) => { |
|
||||
if (err) { |
|
||||
reject(err) |
|
||||
} else { |
|
||||
resolve() |
|
||||
} |
|
||||
}) |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* @param {net.Socket} client |
|
||||
* */ |
|
||||
async function handleClient(client) { |
|
||||
const rl = readline.createInterface({ input: client }) |
|
||||
|
|
||||
/* Added by GS: A stack of katex's `macros` objects, each group inherits |
|
||||
* the one from the parent group and can add its own stuff without |
|
||||
* affecting the parent. |
|
||||
*/ |
|
||||
let macroStack = [{}] |
|
||||
for await (const line of rl) { |
|
||||
try { |
|
||||
// The custom commands for pushing and popping the macro stack.
|
|
||||
if (line === "begingroup") { |
|
||||
// Copy the current state of macros and push it onto the stack.
|
|
||||
macroStack.push({...macroStack.slice(-1)[0]}) |
|
||||
continue |
|
||||
} else if (line === "endgroup") { |
|
||||
macroStack.pop() |
|
||||
continue |
|
||||
} else if (line === "init") { |
|
||||
macroStack = [{}] |
|
||||
continue |
|
||||
} |
|
||||
const query = JSON.parse(line) |
|
||||
const results = [] |
|
||||
for (const input of query.formulas) { |
|
||||
const options = input.options ?? query.options ?? defaultOptions |
|
||||
// Add macros from the macros option
|
|
||||
if (options.macros) { |
|
||||
for (const macro of Object.keys(options.macros)) { |
|
||||
macroStack.slice(-1)[macro] = options.macros[macro] |
|
||||
} |
|
||||
} |
|
||||
options.macros = macroStack.slice(-1)[0] |
|
||||
// Enforce globalGroup option, katex then saves created macros
|
|
||||
// into the options.macros object.
|
|
||||
options.globalGroup = true |
|
||||
try { |
|
||||
const html = katex.renderToString(input.tex, options) |
|
||||
results.push({ html }) |
|
||||
} catch (e) { |
|
||||
results.push({ error: String(e) }) |
|
||||
} |
|
||||
} |
|
||||
await socketWrite(client, JSON.stringify({ results }, null, query.debug ? ' ' : undefined)) |
|
||||
await socketWrite(client, '\n') |
|
||||
} catch (e) { |
|
||||
console.error(e) |
|
||||
await socketWrite(client, JSON.stringify({ error: String(e) })) |
|
||||
await socketWrite(client, '\n') |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
@ -1,39 +0,0 @@ |
|||||
{ |
|
||||
"name": "ksp-katex-server", |
|
||||
"version": "1.0.0", |
|
||||
"lockfileVersion": 3, |
|
||||
"requires": true, |
|
||||
"packages": { |
|
||||
"": { |
|
||||
"name": "ksp-katex-server", |
|
||||
"version": "1.0.0", |
|
||||
"license": "ISC", |
|
||||
"dependencies": { |
|
||||
"katex": "^0.16.3" |
|
||||
} |
|
||||
}, |
|
||||
"node_modules/commander": { |
|
||||
"version": "8.3.0", |
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", |
|
||||
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", |
|
||||
"engines": { |
|
||||
"node": ">= 12" |
|
||||
} |
|
||||
}, |
|
||||
"node_modules/katex": { |
|
||||
"version": "0.16.3", |
|
||||
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.3.tgz", |
|
||||
"integrity": "sha512-3EykQddareoRmbtNiNEDgl3IGjryyrp2eg/25fHDEnlHymIDi33bptkMv6K4EOC2LZCybLW/ZkEo6Le+EM9pmA==", |
|
||||
"funding": [ |
|
||||
"https://opencollective.com/katex", |
|
||||
"https://github.com/sponsors/katex" |
|
||||
], |
|
||||
"dependencies": { |
|
||||
"commander": "^8.0.0" |
|
||||
}, |
|
||||
"bin": { |
|
||||
"katex": "cli.js" |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
@ -1,14 +0,0 @@ |
|||||
{ |
|
||||
"name": "ksp-katex-server", |
|
||||
"version": "1.0.0", |
|
||||
"description": "", |
|
||||
"main": "index.mjs", |
|
||||
"scripts": { |
|
||||
"test": "echo \"Error: no test specified\" && exit 1" |
|
||||
}, |
|
||||
"author": "", |
|
||||
"license": "ISC", |
|
||||
"dependencies": { |
|
||||
"katex": "^0.16.3" |
|
||||
} |
|
||||
} |
|
@ -1,270 +0,0 @@ |
|||||
from panflute import * |
|
||||
import os |
|
||||
from typing import Union |
|
||||
|
|
||||
from .whitespace import NBSP |
|
||||
from .elements import FQuoted |
|
||||
from .util import inlinify |
|
||||
from .context import Group |
|
||||
from .images import ImageProcessor |
|
||||
|
|
||||
# Heavily inspired by: git://git.ucw.cz/labsconf2022.git |
|
||||
def tex(e: Union[Element, ListContainer], 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"] != "tex": |
|
||||
return "" |
|
||||
|
|
||||
if isinstance(e, ListContainer): |
|
||||
return ''.join([tex(child, 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. |
|
||||
content_foot = "" |
|
||||
content_head = "" |
|
||||
|
|
||||
arguments = "" |
|
||||
open = "{" |
|
||||
close = "}" |
|
||||
|
|
||||
tag = e.tag.lower() |
|
||||
|
|
||||
tags = { |
|
||||
Header: "h"+chr(64 + e.level) if isinstance(e, Header) else "", |
|
||||
} |
|
||||
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 \n' |
|
||||
|
|
||||
# Elements which can be represented by a simple string |
|
||||
simple_string = { |
|
||||
NBSP: "~", |
|
||||
Space: " ", |
|
||||
Null: "", |
|
||||
LineBreak: f"\\\\", |
|
||||
SoftBreak: f" ", |
|
||||
HorizontalRule: "\\hr\n\n" |
|
||||
} |
|
||||
if type(e) in simple_string: |
|
||||
return simple_string[type(e)] |
|
||||
|
|
||||
# Simplest basic elements |
|
||||
if isinstance(e, Str): |
|
||||
return e.text.replace(" ", "~") |
|
||||
|
|
||||
if isinstance(e, Para): |
|
||||
return tex(e.content, i, 0, "")+"\n\n" |
|
||||
|
|
||||
if isinstance(e, Span) or isinstance(e, Plain): |
|
||||
return tex(e.content, i, 0, "") |
|
||||
|
|
||||
# Overriding elements with their own returns |
|
||||
if isinstance(e, Image): |
|
||||
url = e.url |
|
||||
|
|
||||
# TODO: This should use OutputGenerator's get_image_processor_args |
|
||||
# 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 ["pdf", "png", "jpeg"]: |
|
||||
# Even supported elements have to be 'converted' because the |
|
||||
# processing contains finding and moving them to the cache |
|
||||
# directory. |
|
||||
url = i.process_image(url, ext, source_dir, **additional_args) |
|
||||
elif ext in ["svg"]: |
|
||||
url = i.process_image(url, "pdf", source_dir, **additional_args) |
|
||||
elif ext in ["epdf"]: |
|
||||
url = i.process_image(url, "pdf", source_dir, **additional_args) |
|
||||
elif ext in ["jpg"]: |
|
||||
url = i.process_image(url, "jpeg", source_dir, **additional_args) |
|
||||
else: |
|
||||
url = i.process_image(url, "pdf", source_dir, **additional_args) |
|
||||
|
|
||||
url = i.find_image(url, [i.cache_dir]) |
|
||||
width = "" |
|
||||
if "width" in e.attributes: |
|
||||
width = e.attributes["width"] |
|
||||
# 50% → 0.5\hsize |
|
||||
if e.attributes["width"][-1] == "%": |
|
||||
width = str(int(e.attributes["width"][:-1])/100) + "\\hsize" |
|
||||
width = "width " + width |
|
||||
return f'\\image{{{width}}}{{{url}}}' |
|
||||
|
|
||||
if isinstance(e, FQuoted): |
|
||||
if e.style == "cs": |
|
||||
if e.quote_type == "SingleQuote": |
|
||||
return f'‚{tex(e.content, i, 0, "")}‘' |
|
||||
elif e.quote_type == "DoubleQuote": |
|
||||
return f'„{tex(e.content, i, 0, "")}“' |
|
||||
elif e.style == "en": |
|
||||
if e.quote_type == "SingleQuote": |
|
||||
return f'‘{tex(e.content, i, 0, "")}’' |
|
||||
elif e.quote_type == "DoubleQuote": |
|
||||
return f'“{tex(e.content, i, 0, "")}”' |
|
||||
else: |
|
||||
if e.quote_type == "SingleQuote": |
|
||||
return f'\'{tex(e.content, i, 0, "")}\'' |
|
||||
elif e.quote_type == "DoubleQuote": |
|
||||
return f'"{tex(e.content, i, 0, "")}"' |
|
||||
else: |
|
||||
return f'"{tex(e.content, i, 0, "")}"' |
|
||||
|
|
||||
if isinstance(e, Code): |
|
||||
return f'\\verb`{e.text.replace("`", "backtick")}`' |
|
||||
|
|
||||
if isinstance(e, Figure): |
|
||||
return f'\\figure{{{tex(e.content, i, indent_level+1, indent_str)}}}{{{tex(e.caption, i, indent_level+1, indent_str)}}}\n\n' |
|
||||
|
|
||||
# Figure caption |
|
||||
if isinstance(e, Caption): |
|
||||
if inlinify(e) is not None: |
|
||||
return f'\\figcaption{{{tex(e.content, i, 0, "")}}}' |
|
||||
|
|
||||
if isinstance(e, Math): |
|
||||
if e.format == "DisplayMath": |
|
||||
return f'$${e.text}$$\n' |
|
||||
else: |
|
||||
return f'${e.text}$' |
|
||||
|
|
||||
# Footnote |
|
||||
if isinstance(e, Note): |
|
||||
tag = "fn" |
|
||||
if inlinify(e) is not None: |
|
||||
return f'\\fn{{{tex(inlinify(e), i, 0, "")}}}' |
|
||||
|
|
||||
if isinstance(e, Table): |
|
||||
aligns = { |
|
||||
"AlignLeft": "\\quad#\\quad\\hfil", |
|
||||
"AlignRight": "\\quad\\hfil#\\quad", |
|
||||
"AlignCenter": "\\quad\\hfil#\\hfil\\quad", |
|
||||
"AlignDefault": "\\quad#\\quad\\hfil" |
|
||||
} |
|
||||
text = "\strut"+"&".join([aligns[col[0]] for col in e.colspec])+"\cr\n" |
|
||||
text += tex(e.head.content, i, 0, "") |
|
||||
text += "\\noalign{\\hrule}\n" |
|
||||
text += tex(e.content[0].content, i, 0, "") |
|
||||
text += "\\noalign{\\hrule}\n" |
|
||||
text += tex(e.foot.content, i, 0, "") |
|
||||
return "\\vskip1em\n\\halign{"+text+"}\n\\vskip1em\n" |
|
||||
# FIXME: Implement rowspan |
|
||||
|
|
||||
if isinstance(e, TableRow): |
|
||||
return "&".join([("\\multispan"+str(cell.colspan)+" " if cell.colspan > 1 else "")+tex(cell.content, i, 0, "") for cell in e.content])+"\cr\n" |
|
||||
|
|
||||
if isinstance(e, RawInline): |
|
||||
if e.format == "tex": |
|
||||
return e.text |
|
||||
else: |
|
||||
return "" |
|
||||
|
|
||||
if isinstance(e, RawBlock): |
|
||||
if e.format == "tex": |
|
||||
return f'{e.text}\n' |
|
||||
else: |
|
||||
return "" |
|
||||
|
|
||||
# See https://pandoc.org/MANUAL.html#line-blocks |
|
||||
if isinstance(e, LineBlock): |
|
||||
return f'{tex(e.content, i, indent_level+1, indent_str)}\n' |
|
||||
|
|
||||
if isinstance(e, LineItem): |
|
||||
return tex(e.content, i, 0, "") + ("\\\\\n" if e.next else "\n") |
|
||||
|
|
||||
if type(e) is Div: |
|
||||
return f'{tex(e.content, i, indent_level+1, indent_str)}' |
|
||||
|
|
||||
if isinstance(e, Doc): |
|
||||
return tex(e.content, i, indent_level, indent_str)+"\n\\bye" # Is having the \bye a bad idea here? |
|
||||
|
|
||||
|
|
||||
# Non-overriding elements, they get generated using the template at the end |
|
||||
# of this function |
|
||||
if isinstance(e, BulletList): |
|
||||
tag = "list" |
|
||||
open = "" |
|
||||
arguments = "{o}" |
|
||||
close = "\\endlist" |
|
||||
|
|
||||
elif isinstance(e, OrderedList): |
|
||||
tag = "list" |
|
||||
open = "" |
|
||||
styles = { |
|
||||
"DefaultStyle": "n", |
|
||||
"Decimal": "n", |
|
||||
"LowerRoman": "i", |
|
||||
"UpperRoman:": "I", |
|
||||
"LowerAlpha": "a", |
|
||||
"UpperAlpha": "A" |
|
||||
} |
|
||||
style = styles[e.style] |
|
||||
delimiters = { |
|
||||
"DefaultDelim": f"{style}.", |
|
||||
"Period": f"{style}.", |
|
||||
"OneParen": f"{style})", |
|
||||
"TwoParens": f"({style})" |
|
||||
} |
|
||||
style = delimiters[e.delimiter] |
|
||||
arguments = f"{{{style}}}" |
|
||||
close = "\\endlist" |
|
||||
# FIXME: Starting number of list |
|
||||
|
|
||||
elif isinstance(e, ListItem): |
|
||||
tag = ":" |
|
||||
|
|
||||
elif isinstance(e, Link): |
|
||||
if len(e.content) == 1 and isinstance(e.content[0], Str) and e.content[0].text == e.url: |
|
||||
tag = "url" |
|
||||
else: |
|
||||
tag = "linkurl" |
|
||||
arguments = f'{{{e.url}}}' |
|
||||
|
|
||||
elif isinstance(e, Group): |
|
||||
tag = "begingroup" |
|
||||
open = "" |
|
||||
if "lang" in e.metadata and e.metadata["lang"] is not None: |
|
||||
open = "\\language"+e.metadata["lang"] |
|
||||
close = "\\endgroup" |
|
||||
|
|
||||
# The default which all non-overriding elements get generated by. This |
|
||||
# includes elements, which were not explicitly mentioned in this function, |
|
||||
# e. g. Strong, Emph... |
|
||||
|
|
||||
if isinstance(e, Inline): |
|
||||
return f'\\{tag}{arguments}{open}{content_head}{tex(e.content, i, 0, "") if hasattr(e, "_content") else ""}{e.text if hasattr(e, "text") else ""}{content_foot}{close}' |
|
||||
|
|
||||
out_str = "" |
|
||||
out_str = f"\\{tag}{arguments}{open}\n" |
|
||||
out_str += content_head |
|
||||
if hasattr(e, "_content"): |
|
||||
out_str += tex(e.content, i, indent_level+1, indent_str) |
|
||||
if hasattr(e, "text"): |
|
||||
out_str += e.text |
|
||||
out_str += f"{content_foot}\n{close}\n\n" |
|
||||
|
|
||||
return out_str |
|
@ -1,176 +0,0 @@ |
|||||
from panflute import Element, Div, Span, Quoted, Image, CodeBlock, Str, MetaInlines, MetaString, MetaBool, RawBlock |
|
||||
import re |
|
||||
import os |
|
||||
|
|
||||
# Import local files |
|
||||
from .whitespace import Whitespace, NBSP, bavlna |
|
||||
from .util import nullify, import_md |
|
||||
from .context import Context, BlockGroup |
|
||||
from .command import Command, BlockCommand, InlineCommand |
|
||||
from .command_util import handle_command_define, parse_command |
|
||||
from .elements import FQuoted |
|
||||
|
|
||||
|
|
||||
import warnings |
|
||||
warnings.warn("The transform function has been deprecated, is left only for reference and will be removed in future commits. TransformProcessor should be used in its place.", DeprecationWarning) |
|
||||
|
|
||||
# This is where tha magic happens. This function transforms a single element, |
|
||||
# to transform the entire tree, panflute's walk should be used. |
|
||||
def transform(e: Element, c: Context) -> Element: |
|
||||
|
|
||||
warnings.warn("The transform function has been deprecated, is left only for reference and will be removed in future commits. TransformProcessor should be used in its place.", DeprecationWarning) |
|
||||
# Determine if this space should be non-breakable. See whitespace.py. |
|
||||
if isinstance(e, Whitespace) and bavlna(e, c): |
|
||||
e = NBSP() |
|
||||
|
|
||||
if hasattr(e, "attributes"): |
|
||||
# `if` attribute. Only show this element if flag is set. |
|
||||
if "if" in e.attributes: |
|
||||
if not c.is_flag_set(e.attributes["if"]): |
|
||||
return nullify(e) |
|
||||
# `ifn` attribute. Only show this element if flag is NOT set |
|
||||
if "ifn" in e.attributes: |
|
||||
if c.is_flag_set(e.attributes["ifn"]): |
|
||||
return nullify(e) |
|
||||
|
|
||||
# There are multiple ways to call a command so we turn it into a |
|
||||
# unified element first and then call it at the end. This handles the |
|
||||
# []{c=commandname} and |
|
||||
# :::{c=commandname} |
|
||||
# ::: |
|
||||
# syntax. |
|
||||
if (isinstance(e, Div) or isinstance(e, Span)) and "c" in e.attributes: |
|
||||
if isinstance(e, Div): |
|
||||
e = BlockCommand(*e.content, identifier=e.identifier, classes=e.classes, attributes=e.attributes) |
|
||||
else: |
|
||||
e = InlineCommand(*e.content, identifier=e.identifier, classes=e.classes, attributes=e.attributes) |
|
||||
|
|
||||
# Isolated subdocuments using Group and a different Context. Can be |
|
||||
# separate files (using attribute `partial`) or be inline using the |
|
||||
# following syntax: |
|
||||
# ```markdown {.group} |
|
||||
# * file content * |
|
||||
# ``` |
|
||||
# Both can contain their own metadata in a FrontMatter (YAML header) |
|
||||
if (isinstance(e, Div) and "partial" in e.attributes)\ |
|
||||
or (isinstance(e, CodeBlock) and "markdown" in e.classes and "group" in e.classes): |
|
||||
if isinstance(e, Div): |
|
||||
if not c.trusted: # If we're in an untrusted context, we shouldn't allow inclusion of files outside the PWD. |
|
||||
full_path = os.path.abspath(c.dir + "/" + e.attributes["partial"]) |
|
||||
pwd = os.path.abspath(".") |
|
||||
if os.path.commonpath([full_path, pwd]) != os.path.commonpath([pwd]): |
|
||||
return nullify(e) |
|
||||
text = open(c.dir + "/" + e.attributes["partial"], "r").read() |
|
||||
path = c.dir + "/" + e.attributes["partial"] |
|
||||
else: |
|
||||
text = e.text |
|
||||
path = c.path |
|
||||
if "type" in e.attributes and e.attributes["type"] in ["tex", "html"]: |
|
||||
e = RawBlock(text, e.attributes["type"]) |
|
||||
else: |
|
||||
includedDoc = import_md(text) |
|
||||
trusted = True |
|
||||
if "untrusted" in e.attributes and (e.attributes["untrusted"] == True or e.attributes["untrusted"] == 'True'): |
|
||||
trusted = False |
|
||||
if not c.trusted: |
|
||||
trusted = False |
|
||||
nContext = Context(includedDoc, path, c, trusted=trusted) |
|
||||
language = includedDoc.get_metadata("lang") |
|
||||
includedDoc = includedDoc.walk(transform, nContext) |
|
||||
e = BlockGroup(*includedDoc.content, context=nContext, metadata={"lang": language}) |
|
||||
|
|
||||
# Transform panflute's Quoted to custom FQuoted, see above. |
|
||||
if isinstance(e, Quoted): |
|
||||
quote_styles = { |
|
||||
"cs": "cs", |
|
||||
"en": "en", |
|
||||
"sk": "cs", |
|
||||
None: None |
|
||||
} |
|
||||
e = FQuoted(*e.content, quote_type=e.quote_type, style=quote_styles[c.get_metadata("lang")]) |
|
||||
|
|
||||
if isinstance(e, Image): |
|
||||
# Pass down the directory of the current source file for finding image |
|
||||
# files. |
|
||||
e.attributes["source_dir"] = c.dir |
|
||||
# Pass down "no-srcset" metadatum as attribute down to images. |
|
||||
if not "no-srcset" in e.attributes: |
|
||||
e.attributes["no-srcset"] = c.get_metadata("no-srcset") if c.get_metadata("no-srcset") is not None else False |
|
||||
|
|
||||
# Pass down metadata 'highlight' and 'highlight_style' as attribute to CodeBlocks |
|
||||
if isinstance(e, CodeBlock): |
|
||||
if not "highlight" in e.attributes: |
|
||||
e.attributes["highlight"] = c.get_metadata("highlight") if c.get_metadata("highlight") is not None else True |
|
||||
if not "style" in e.attributes: |
|
||||
e.attributes["style"] = c.get_metadata("highlight-style") if c.get_metadata("highlight-style") is not None else "default" |
|
||||
e.attributes["noclasses"] = False |
|
||||
# I think this is supposed to enable inline styles for highlighting when the style differs from the document, but it clearly doesn't work. a) HTML_generator never accesses it and b) Only the top-level document contains a style so you have to ask the top level context, not the current context. |
|
||||
else: |
|
||||
e.attributes["noclasses"] = True |
|
||||
|
|
||||
# Execute python code inside source code block. Works the same as commands. |
|
||||
# Syntax: |
|
||||
# ```python {.run} |
|
||||
# print("woo") |
|
||||
# ``` |
|
||||
if isinstance(e, CodeBlock) and hasattr(e, "classes") and "python" in e.classes and "run" in e.classes: |
|
||||
if not c.trusted: |
|
||||
return nullify(e) |
|
||||
command_output = parse_command(e.text)(BlockCommand(), c) |
|
||||
e = BlockCommand().replaceSelf(*([] if command_output is None else command_output)) |
|
||||
e = e.walk(transform, c) |
|
||||
|
|
||||
# Command defines for calling using BlockCommand and InlineCommand. If |
|
||||
# redefine is used instead of define, the program doesn't check if the |
|
||||
# command already exists. |
|
||||
# Syntax: |
|
||||
# ```python {define=commandname} |
|
||||
# print(wooo) |
|
||||
# ``` |
|
||||
if isinstance(e, CodeBlock) and hasattr(e, "classes") and "python" in e.classes and hasattr(e, "attributes")\ |
|
||||
and ("define" in e.attributes or "redefine" in e.attributes): |
|
||||
if not c.trusted: |
|
||||
return nullify(e) |
|
||||
e = handle_command_define(e, c) |
|
||||
|
|
||||
## Shorthands |
|
||||
# Shorter (and sometimes the only) forms of certain features |
|
||||
if isinstance(e, Span) and len(e.content) == 1 and isinstance(e.content[0], Str): |
|
||||
## Handle special command shorthand [!commandname]{} |
|
||||
if re.match(r"^![\w.]+$", e.content[0].text): |
|
||||
e = InlineCommand(identifier=e.identifier, classes=e.classes, attributes={**e.attributes, "c": e.content[0].text[1:]}) |
|
||||
|
|
||||
## Handle import [#path/file.md]{} |
|
||||
# This is the exact opposite of partials. We take the commands, flags |
|
||||
# and metadata but drop the content. |
|
||||
elif re.match(r"^#.+$", e.content[0].text): |
|
||||
importedDoc = import_md(open(c.dir + "/" + e.content[0].text[1:], "r").read()) |
|
||||
importedDoc.walk(transform, c) |
|
||||
return nullify(e) |
|
||||
|
|
||||
## Handle metadata print [$key1.key2]{} |
|
||||
# This is a shorthand for just printing the content of some metadata. |
|
||||
elif re.match(r"^\$[\w.]+$", e.content[0].text): |
|
||||
val = c.get_metadata(e.content[0].text[1:], False) |
|
||||
if isinstance(val, MetaInlines): |
|
||||
e = Span(*val.content) |
|
||||
e = e.walk(transform, c) |
|
||||
elif isinstance(val, MetaString): |
|
||||
e = Span(Str(val.string)) |
|
||||
elif isinstance(val, MetaBool): |
|
||||
e = Span(Str(str(val.boolean))) |
|
||||
else: |
|
||||
raise TypeError(f"Cannot print value of metadatum '{e.content[0].text[1:]}' of type '{type(val)}'") |
|
||||
|
|
||||
## Execute commands |
|
||||
# panflute's walk function transforms the children first, then the root |
|
||||
# element, so the content the command receives is already transformed. |
|
||||
# The output from the command is then transformed manually again. |
|
||||
if isinstance(e, Command): |
|
||||
if not c.get_command(e.attributes["c"]): |
|
||||
raise NameError(f"Command not defined '{e.attributes['c']}'.") |
|
||||
command_output = c.get_command(e.attributes["c"])(e, c) |
|
||||
e = e.replaceSelf(*command_output) |
|
||||
e = e.walk(transform, c) |
|
||||
|
|
||||
return e |
|
@ -1,8 +0,0 @@ |
|||||
<!DOCTYPE html> |
|
||||
<html> |
|
||||
<head> |
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"> |
|
||||
<meta charset='utf-8'> |
|
||||
<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/katex@0.16.4/dist/katex.min.css' integrity='sha384-vKruj+a13U8yHIkAyGgK1J3ArTLzrFGBbBc0tDp4ad/EyewESeXE/Iv67Aj8gKZ0' crossorigin='anonymous'> |
|
||||
</head> |
|
||||
<body> |
|
Loading…
Reference in new issue