Compare commits
No commits in common. "master" and "f3309d65692e4ab4e4b1f16fab1814ba67b721bf" have entirely different histories.
master
...
f3309d6569
50 changed files with 1424 additions and 3387 deletions
|
@ -1,7 +0,0 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
charset = utf-8
|
||||
indent_style = tab
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -10,8 +10,3 @@ public/
|
|||
*.jpeg
|
||||
*.svg
|
||||
!test/1px.png
|
||||
**/.mypy_cache
|
||||
**/*.egg-info
|
||||
build/
|
||||
dist/
|
||||
**/node_modules
|
||||
|
|
3
.gitmodules
vendored
3
.gitmodules
vendored
|
@ -1,6 +1,3 @@
|
|||
[submodule "ucwmac"]
|
||||
path = ucwmac
|
||||
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
|
||||
|
|
213
README.md
213
README.md
|
@ -16,9 +16,7 @@ Inkscape are used for image processing. Nodejs is used for KaTeX.
|
|||
|
||||
## Usage
|
||||
```
|
||||
usage: formatitko [-h] [-l IMG_LOOKUP_DIRS [IMG_LOOKUP_DIRS ...]] [-p IMG_PUBLIC_DIR] [-c IMG_CACHE_DIR] [-i IMG_WEB_PATH] [-w OUTPUT_HTML] [-t OUTPUT_TEX] [-m OUTPUT_MD]
|
||||
[-j OUTPUT_JSON] [--katex-server] [-k KATEX_SOCKET] [--debug]
|
||||
input_filename
|
||||
usage: formatitko.py [-h] [-l IMG_LOOKUP_DIRS [IMG_LOOKUP_DIRS ...]] [-p IMG_PUBLIC_DIR] [-i IMG_WEB_PATH] [-w OUTPUT_HTML] [-t OUTPUT_TEX] input_filename
|
||||
|
||||
positional arguments:
|
||||
input_filename The markdown file to process.
|
||||
|
@ -26,27 +24,16 @@ positional arguments:
|
|||
options:
|
||||
-h, --help show this help message and exit
|
||||
-l IMG_LOOKUP_DIRS [IMG_LOOKUP_DIRS ...], --img-lookup-dirs IMG_LOOKUP_DIRS [IMG_LOOKUP_DIRS ...]
|
||||
Image lookup directories. When processing images, the program will try to find the image in them first. Always looks for images in the same folder
|
||||
as the markdown file. (default: [])
|
||||
Image lookup directories. When processing images, the program will try to find the image in them first. Always looks for images in the same folder as the markdown
|
||||
file. (default: [])
|
||||
-p IMG_PUBLIC_DIR, --img-public-dir IMG_PUBLIC_DIR
|
||||
Directory to put processed images into. The program will overwrite images, whose dependencies are newer. (default: public)
|
||||
-c IMG_CACHE_DIR, --img-cache-dir IMG_CACHE_DIR
|
||||
Directory to cache processed images and intermediate products. The program will overwrite files, whose dependencies are newer. (default: cache)
|
||||
Directory to put processed images into. The program will not overwrite existing images. (default: public)
|
||||
-i IMG_WEB_PATH, --img-web-path IMG_WEB_PATH
|
||||
Path where the processed images are available on the website. (default: /)
|
||||
-w OUTPUT_HTML, --output-html OUTPUT_HTML
|
||||
The HTML file (for Web) to write into. (default: None)
|
||||
The HTML file (for Web) to write into. (default: output.html)
|
||||
-t OUTPUT_TEX, --output-tex OUTPUT_TEX
|
||||
The TEX file to write into. (default: None)
|
||||
-m OUTPUT_MD, --output-md OUTPUT_MD
|
||||
The Markdown file to write into. (Uses pandoc to generate markdown) (default: None)
|
||||
-j OUTPUT_JSON, --output-json OUTPUT_JSON
|
||||
The JSON file to dump the pandoc-compatible AST into. (default: None)
|
||||
--katex-server Starts a KaTeX server and prints the socket filename onto stdout. Useful for running formatitko many times without starting the KaTeX server each
|
||||
time. (default: False)
|
||||
-k KATEX_SOCKET, --katex-socket KATEX_SOCKET
|
||||
The KaTeX server socket filename obtained by running with `--katex-server`. (default: None)
|
||||
--debug
|
||||
The TEX file to write into. (default: output.tex)
|
||||
```
|
||||
|
||||
## Format
|
||||
|
@ -59,7 +46,7 @@ definition lists and citations. It also adds its own custom features.
|
|||
|
||||
Flags can be set in the Front Matter or with python code. Then, elements with
|
||||
the `if` attribute will only be shown if the flag is set to True and elements
|
||||
with the `ifnot` attribute will only be show if the flag is not set to True.
|
||||
with the `ifn` attribute will only be show if the flag is not set to True.
|
||||
|
||||
**Example:**
|
||||
|
||||
|
@ -72,7 +59,7 @@ flags:
|
|||
|
||||
[This will not be shown]{if=bar}
|
||||
|
||||
[This will be shown]{ifnot=bar}
|
||||
[This will be shown]{ifn=bar}
|
||||
```
|
||||
|
||||
### Including other files
|
||||
|
@ -82,28 +69,12 @@ There are two ways of including files.
|
|||
#### Importing
|
||||
The first is importing, which only takes the state (defined commands, metadata,
|
||||
etc.) from the file and any content is omitted. This is useful for creating
|
||||
libraries of commands.
|
||||
libraries of commands. The syntax is as follows:
|
||||
|
||||
There are three types of imports:
|
||||
[#test/empty.md]{}
|
||||
|
||||
##### Python Module (the default)
|
||||
```markdown
|
||||
[#ksp_formatitko as ksp]{}
|
||||
```
|
||||
or
|
||||
```markdown
|
||||
[#ksp_formatitko]{}
|
||||
```
|
||||
with an optional `type=module` in the curly brackets, tries to import a python
|
||||
module as a set of formatitko commands. See below for more details about
|
||||
commands.
|
||||
|
||||
##### JSON Metadata
|
||||
[#test/test.json]{type=metadata key=orgs}
|
||||
|
||||
This will import metadata from a JSON file. THe optional `key` argument sets the
|
||||
key under which the whole JSON file will be placed. Dictionaries are merged,
|
||||
others overwritten.
|
||||
The curly braces are required for pandoc to parse the import properly and should
|
||||
be left empty.
|
||||
|
||||
#### Partials
|
||||
Partials are the very opposite of imports, they have their own context, which
|
||||
|
@ -124,19 +95,12 @@ partial to `tex` or `html`.
|
|||
### Groups
|
||||
|
||||
Groups are pieces of markdown with their own sandboxed context, in other words,
|
||||
inline partials. Syntax-wise they are pandoc Divs with the `.group` class. All
|
||||
attributes of the Div will be passed down as metadata to the group.
|
||||
|
||||
::: {.group lang=cs}
|
||||
OOOoo český mód
|
||||
:::
|
||||
|
||||
If you want to have more fancy metadata, that can only be specified in a front
|
||||
matter, you can use the following syntax:
|
||||
inline partials. They function exactly the same as partials, namely can have
|
||||
their own front matter.
|
||||
|
||||
```markdown {.group}
|
||||
---
|
||||
lang: cs
|
||||
language: cs
|
||||
---
|
||||
OOOoo český mód
|
||||
```
|
||||
|
@ -150,9 +114,6 @@ fmt.Pritln("owo")
|
|||
```
|
||||
````
|
||||
|
||||
Note however, that when this syntax is used, pandoc is executed for each of
|
||||
these blocks which could get slow. Using divs is preferred.
|
||||
|
||||
Groups and partials are also enclosed in `\begingroup` and `\endgroup` in the
|
||||
output TeX.
|
||||
|
||||
|
@ -177,38 +138,15 @@ pandoc feature.]
|
|||
### Running python code
|
||||
|
||||
Formátítko allows you to run Python code directly from your MD file. Any
|
||||
`python` code block with the class `run` will be executed.
|
||||
`python` code block with the class `run` will be executed:
|
||||
|
||||
#### Command environment
|
||||
#### Context
|
||||
|
||||
The commands will be executed as functions with the following signature:
|
||||
```python
|
||||
def command(element: Command, context: Context) -> list[Element]:
|
||||
```
|
||||
some global variables may be available, and are defined in `command_env.py`:
|
||||
```python
|
||||
import panflute as pf
|
||||
import formatitko.elements as fe
|
||||
from formatitko.util import import_md_list
|
||||
from formatitko.util import parse_string
|
||||
|
||||
from formatitko.context import Context
|
||||
from formatitko.command import Command
|
||||
from panflute import Element
|
||||
```
|
||||
##### `element` parameter
|
||||
|
||||
The `element` parameter holds the element the command is currently being executed
|
||||
on. In the case of running python blocks directly, it is probably not
|
||||
interesting but will get interesting later.
|
||||
|
||||
##### `context` parameter
|
||||
|
||||
You can access the current context using the `context` parameter. The context
|
||||
You can access the current context using the `ctx` variable. The context
|
||||
provides read/write access to the FrontMatter metadata. The context has the
|
||||
following methods:
|
||||
|
||||
`context.get_metadata(key: str, simple: bool=True, immediate: bool=False)`
|
||||
`ctx.get_metadata(key: str, simple: bool=True, immediate: bool=False)`
|
||||
|
||||
- `key`: The key of the metadatum you want to get. Separate child keys with
|
||||
dots: `ctx.get_metadata("flags.foo")`
|
||||
|
@ -218,13 +156,13 @@ following methods:
|
|||
- `immediate`: Only get metadatum from the current context, not from its
|
||||
parents.
|
||||
|
||||
`context.set_metadata(key: str, value)`
|
||||
`ctx.set_metadata(key: str, value)`
|
||||
|
||||
- `key`: The key of the metadatum you want to get. Separate child keys with
|
||||
dots: `ctx.get_metadata("flags.foo")`
|
||||
- `value`: Any value you want to assign to the metadatum
|
||||
|
||||
`context.unset_metadata(key: str)`
|
||||
`ctx.unset_metadata(key: str)`
|
||||
|
||||
Delete the metadatum in the current context and allow it to inherit the value
|
||||
from the parent context.
|
||||
|
@ -234,31 +172,26 @@ from the parent context.
|
|||
|
||||
Helper functions for flags exist which work the same as for metadata:
|
||||
|
||||
`context.is_flag_set(flag: str) -> bool`
|
||||
`ctx.is_flag_set(flag: str) -> bool`
|
||||
|
||||
`context.set_flag(flag: str, val: bool)`
|
||||
`ctx.set_flag(flag: str, val: bool)`
|
||||
|
||||
`context.unset_flag(flag: str)`
|
||||
`ctx.unset_flag(flag: str)`
|
||||
|
||||
There are also other useful functions, which you can see for yourself in
|
||||
`context.py`.
|
||||
#### Writing output
|
||||
|
||||
> **WARNING**: Writing to metadata should **only** be done **at the beginning**
|
||||
> of the document or a group (before any printable content). Writing to metadata
|
||||
> in other places in the document might cause undefined behaviour (mostly some
|
||||
> elements might behave as if the metadata was set elsewhere).
|
||||
There are two modes of writing output, plaintext and element-based.
|
||||
|
||||
##### Return value
|
||||
The function **must** return a list of valid Elements. This list may be empty.
|
||||
These elements will be placed in the document in the location where the command
|
||||
was invoked.
|
||||
Plaintext mode uses the `print(text: str)` and `println(text: str)` functions,
|
||||
that append text to a buffer which is then interpreted as markdown input.
|
||||
|
||||
The `parse_string` function might be useful, it turns a simple string into a
|
||||
list of panflute's `Str`s and `Space`s (without any formatting). If you want to
|
||||
use markdown in your function output, you have to convert it yourself using
|
||||
`import_md` but beware this calls pandoc, is potentially slow and is
|
||||
discouraged.
|
||||
Element-based mode uses the `appendChild(element: pf.Element)` and
|
||||
`appendChildren(*elements: List[pf.Element])` functions which allow you to
|
||||
append `panflute` elements to a list which is then again interpreted as input.
|
||||
The `panflute` library is available as `pf`.
|
||||
|
||||
When one of these functions is called, the mode is set and functions from the
|
||||
other mode cannot be called within the same block of code.
|
||||
|
||||
**Examples:**
|
||||
|
||||
|
@ -267,15 +200,14 @@ discouraged.
|
|||
title: Foo
|
||||
---
|
||||
```python {.run}
|
||||
return [
|
||||
pf.Para(pf.Emph(pf.Str("wooo"))),
|
||||
pf.Para(*parse_string("The title of this file is: " + context.get_metadata("title")))
|
||||
]
|
||||
println("*wooo*")
|
||||
println()
|
||||
println("The title of this file is: " + ctx.get_metadata("title"))
|
||||
```
|
||||
````
|
||||
|
||||
```python {.run}
|
||||
return [pf.Strong(*parse_string("Hello world!"))]
|
||||
appendChild(pf.Para(pf.Strong(pf.Str("foo"))))
|
||||
```
|
||||
|
||||
### Defining and running commands
|
||||
|
@ -286,7 +218,7 @@ Code blocks can be also saved and executed later. Defining is done using the
|
|||
**Example:**
|
||||
|
||||
```python {define=commandname}
|
||||
return [pf.Str("foo")]
|
||||
print("foo")
|
||||
```
|
||||
|
||||
If you try to define the same command twice, you will get an error. To redefine
|
||||
|
@ -298,7 +230,7 @@ There are multiple ways of running commands. There is the shorthand way:
|
|||
|
||||
[!commandname]{}
|
||||
|
||||
Or using the `c` attribute on a span or a div (new: or a codeblock!):
|
||||
Or using the `c` attribute on a span or a div:
|
||||
|
||||
[Some content]{c=commandname}
|
||||
|
||||
|
@ -306,16 +238,6 @@ Or using the `c` attribute on a span or a div (new: or a codeblock!):
|
|||
Some content
|
||||
:::
|
||||
|
||||
```python {define=bash}
|
||||
import subprocess
|
||||
c = subprocess.run(["bash", "-c", element.text], stdout=subprocess.PIPE, check=True, encoding="utf-8")
|
||||
return [pf.Para(pf.Str(c.stdout))]
|
||||
```
|
||||
|
||||
```bash {c=bash}
|
||||
cat /etc/hostname
|
||||
```
|
||||
|
||||
To access the content or attributes of the div or span the command has been
|
||||
called on, the `element` variable is available, which contains the `panflute`
|
||||
representation of the element.
|
||||
|
@ -323,7 +245,7 @@ representation of the element.
|
|||
**Example:**
|
||||
|
||||
```python {define=index}
|
||||
return [element.content[int(element.attributes["i"])]]
|
||||
appendChild(element.content[int(element.attributes["i"])])
|
||||
```
|
||||
|
||||
[Pick the third element from this span]{c=index i=2}
|
||||
|
@ -346,24 +268,23 @@ blocks. To turn it off for a single block, don't specify a language or set the
|
|||
`highlight` attribute to `False`. You can also set the metadatum `highlight` to
|
||||
`false` in the FrontMatter to disable it in a given Group. To change the [highlighting
|
||||
style](https://pygments.org/styles/), you have to set the `highlight-style`
|
||||
metadatum or the `style` attribute directly on the element.
|
||||
metadatum in the **top-level document** this is to prevent the need for many
|
||||
inline style definitions.
|
||||
|
||||
**Examples:**
|
||||
```python
|
||||
print("cool")
|
||||
```
|
||||
|
||||
```python {style=manni}
|
||||
print("freezing")
|
||||
```
|
||||
|
||||
```zsh {highlight=False}
|
||||
./formatitko.py README.md
|
||||
```
|
||||
|
||||
### Language awareness
|
||||
Formátítko is language aware, this means that the `lang` metadatum is
|
||||
somewhat special. (It is also special for pandoc)
|
||||
Formátítko is language aware, this means that the `language` metadatum is
|
||||
somewhat special. When set using the front matter, it is also popped out to TeX
|
||||
as a `\languagexx` macro. Currently supported values are `cs` and `en` for
|
||||
internal uses but can be set to anything.
|
||||
|
||||
### NBSP
|
||||
Formátítko automatically inserts no-break spaces according to its sorta smart
|
||||
|
@ -382,9 +303,12 @@ language.
|
|||
|
||||
**Examples:**
|
||||
|
||||
::: {.group lang=cs}
|
||||
```markdown {.group}
|
||||
---
|
||||
language: cs
|
||||
---
|
||||
"Uvozovky se v českém testu píší 'jinak' než v angličtině."
|
||||
:::
|
||||
```
|
||||
|
||||
"In Czech texts, quotes are written 'differently' than in English"
|
||||
|
||||
|
@ -415,9 +339,6 @@ Images are automatically searched for in the directory where each markdown file
|
|||
command line parameter. After processing, they're all put into the folder
|
||||
specified with `--public-dir`.
|
||||
|
||||
Formátítko also does dependency management, which means that all images will be
|
||||
regenerated only when their dependencies are newer.
|
||||
|
||||
#### Image processing
|
||||
Images are automatically processed so that they can be successfully used in both
|
||||
output formats. This includes generating multiple sizes and providing a
|
||||
|
@ -427,22 +348,12 @@ To customize this, the `file-width`, `file-height`, `file-dpi`, `file-quality`
|
|||
and `no-srcset` attributes are available. All but the last one should be
|
||||
integers.
|
||||
|
||||
Processing also includes Asymptote images -- you can simply include an asymptote
|
||||
program as an image and formátítko handles the rest for you.
|
||||
|
||||
#### Content headers and footers
|
||||
|
||||
If you want formatitko to generate fully formed html files for you, you might
|
||||
want to add a HTML partial with the starting tags and `<head>`. This would
|
||||
normally not work, because the entire document is wrapped with `<main>`. Using
|
||||
the special `.header_content` and `.footer_content` classes of divs, you can
|
||||
append content to a header and footer, which are popped to the output before and
|
||||
after the document.
|
||||
|
||||
:::: {.header_content}
|
||||
::: {partial="test/test-top.html" type="html"}
|
||||
:::
|
||||
::::
|
||||
Keep in mind that the processing tries to be as lazy as possible, so it never
|
||||
overwrites any files and if it finds the right format or resolution (only
|
||||
judging by the filenames) in the lookup directories it will just copy that. This
|
||||
means that any automatic attempts at conversion can be overridden by converting
|
||||
the file yourself, naming it accordingly and placing it either in the public or
|
||||
one of the lookup directories.
|
||||
|
||||
## Working with the produced output
|
||||
|
||||
|
@ -455,15 +366,11 @@ your `<head>`^[This is taken directly from [KaTeX's docs](https://katex.org/docs
|
|||
<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'>
|
||||
```
|
||||
|
||||
You can see how this is done in `test/test.md`
|
||||
Also the output HTML is not intended as a standalone file but should be included
|
||||
as part of a larger template. (That includes a doctype, other css, etc.)
|
||||
|
||||
### TeX
|
||||
The TeX output is not usable as is. Many of the elements are just converted to
|
||||
macros, which you have to define yourself. There is an example implementation in
|
||||
`formatitko.tex`, which uses LuaTeX and the ucwmac package, but you should
|
||||
customize it to your needs (and to the context in which the output is used).
|
||||
|
||||
## More examples
|
||||
|
||||
More usage examples can be found (even though a bit chaotically) in the test
|
||||
directory.
|
||||
|
|
95
command.py
Normal file
95
command.py
Normal file
|
@ -0,0 +1,95 @@
|
|||
from panflute import Div,Span,Para
|
||||
from typing import List
|
||||
|
||||
# Import local files
|
||||
from util import *
|
||||
from context import Context
|
||||
from mj_show import show
|
||||
|
||||
class Command:
|
||||
pass
|
||||
|
||||
# This distinction is needed because while transforming the tree, inline
|
||||
# elements cannot be replaced with block ones
|
||||
class InlineCommand(Span, Command):
|
||||
def replaceSelf(self, content: List[Element]) -> Span:
|
||||
try:
|
||||
return Span(*content)
|
||||
except TypeError:
|
||||
if len(content) == 1 and isinstance(content[0], Para):
|
||||
return Span(*content[0].content)
|
||||
else:
|
||||
raise SyntaxError(f"The command {self.attributes['c']} returned multiple Paragraphs and must be executed using `::: {{c={self.attributes['c']}}}\\n:::`.")
|
||||
pass
|
||||
|
||||
class BlockCommand(Div, Command):
|
||||
def replaceSelf(self, content: List[Element]) -> Div:
|
||||
return Div(*content)
|
||||
pass
|
||||
|
||||
# This function is called in trasform.py, defining a command which can be
|
||||
# called later using the function below
|
||||
def handle_command_define(e: Element, c: Context):
|
||||
if "define" in e.attributes:
|
||||
if not c.get_command(e.attributes["define"]):
|
||||
c.set_command(e.attributes["define"], compile(e.text, '<string>', 'exec'))
|
||||
return nullify(e)
|
||||
else:
|
||||
raise NameError(f"Command already defined: '{e.attributes['define']}'")
|
||||
if "redefine" in e.attributes:
|
||||
c.set_command(e.attributes["redefine"], compile(e.text, '<string>', 'exec'))
|
||||
return nullify(e)
|
||||
return e
|
||||
|
||||
# This function executes commands and inline runnable code blocks (see
|
||||
# transform.py for their syntax). Context can be accessed using `ctx` and there
|
||||
# are four functions available to create output from these commands and the
|
||||
# element the command has been called on (including its .content) can be
|
||||
# accessed using `element`. Arguments can be passed down to the comand using
|
||||
# the element's attributes.
|
||||
#
|
||||
# print and println append text to a buffer which is then interpreted as
|
||||
# markdown with the current context.
|
||||
#
|
||||
# appendChild and appendChildren append panflute elements to a list which is
|
||||
# then transformed. A command which does nothing looks like this:
|
||||
# ```python {define=nop}
|
||||
# appendChildren(element.content)
|
||||
# ```
|
||||
#
|
||||
# These two types, appending and printing, cannot be mixed.
|
||||
|
||||
def executeCommand(source, element: Element, ctx: Context) -> List[Element]:
|
||||
mode = 'empty'
|
||||
text = ""
|
||||
content = []
|
||||
def print(s: str=""):
|
||||
nonlocal mode, text
|
||||
if mode == 'elements':
|
||||
raise SyntaxError("Cannot use `print` and `appendChild` in one command at the same time.")
|
||||
mode = 'text'
|
||||
text += str(s)
|
||||
|
||||
def println(s: str=""):
|
||||
print(str(s)+"\n")
|
||||
|
||||
def appendChild(e: Element):
|
||||
nonlocal mode, content
|
||||
if mode == 'text':
|
||||
raise SyntaxError("Cannot use `print` and `appendChild` in one command at the same time.")
|
||||
mode = 'elements'
|
||||
content.append(e)
|
||||
|
||||
def appendChildren(l: List[Element]):
|
||||
for e in l:
|
||||
appendChild(e)
|
||||
|
||||
import panflute as pf
|
||||
exec(source)
|
||||
|
||||
if mode == 'text':
|
||||
return import_md(text, standalone=False)
|
||||
if mode == 'elements':
|
||||
return content
|
||||
|
||||
return []
|
94
context.py
Normal file
94
context.py
Normal file
|
@ -0,0 +1,94 @@
|
|||
|
||||
from panflute import Doc, Div
|
||||
from typing import Dict
|
||||
import os
|
||||
|
||||
|
||||
# This class is used to keep state while transforming the document using
|
||||
# transform.py. For the context to be available to the html and TeX generators,
|
||||
# individual keys must be manually assigned to the individual elements. This is
|
||||
# done in transform.py.
|
||||
#
|
||||
# The context is also aware of its parent contexts and relevant data (such as
|
||||
# metadata and commands) can be read from the closest parent context. Writing
|
||||
# only happens to the current one.
|
||||
#
|
||||
# This class is basically an extension to panflute's doc, this is why metadata
|
||||
# is read directly from it.
|
||||
class Context:
|
||||
def __init__(self, doc: Doc, path: str, parent: 'Context'=None, trusted: bool=True):
|
||||
self.parent = parent
|
||||
self._commands = {}
|
||||
self.doc = doc
|
||||
self.trusted = trusted
|
||||
self.path = path
|
||||
self.dir = os.path.dirname(path) if os.path.dirname(path) != "" else "."
|
||||
self.filename = os.path.basename(path)
|
||||
if self.get_metadata("flags", immediate=True) is None:
|
||||
self.set_metadata("flags", {})
|
||||
|
||||
def get_command(self, command: str):
|
||||
if command in self._commands:
|
||||
return self._commands[command]
|
||||
elif self.parent:
|
||||
return self.parent.get_command(command)
|
||||
else:
|
||||
return None
|
||||
|
||||
def set_command(self, command: str, val):
|
||||
self._commands[command] = val
|
||||
|
||||
def unset_command(self, command: str):
|
||||
del self._commands[command]
|
||||
|
||||
def is_flag_set(self, flag: str):
|
||||
if self.get_metadata("flags."+flag):
|
||||
if self.get_metadata("flags."+flag):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
elif self.parent:
|
||||
return self.parent.is_flag_set(flag)
|
||||
else:
|
||||
return False
|
||||
|
||||
def set_flag(self, flag: str, val: bool):
|
||||
self.set_metadata("flags."+flag, val)
|
||||
|
||||
def unset_flag(self, flag: str):
|
||||
self.unset_metadata("flags."+flag)
|
||||
|
||||
def get_metadata(self, key: str, simple: bool=True, immediate: bool=False):
|
||||
value = self.doc.get_metadata(key, None, simple)
|
||||
if value is not None:
|
||||
return value
|
||||
elif self.parent and not immediate:
|
||||
return self.parent.get_metadata(key)
|
||||
else:
|
||||
return None
|
||||
|
||||
def set_metadata(self, key: str, value):
|
||||
if key == "language":
|
||||
print("WARN: Setting language this way doesn't propagate to TeX. Either use the Front Matter or specify it additionally using the \\languagexx macro.")
|
||||
meta = self.doc.metadata
|
||||
key = key.split(".")
|
||||
for k in key[:-1]:
|
||||
meta = meta[k]
|
||||
meta[key[-1]] = value
|
||||
|
||||
def unset_metadata(self, key: str):
|
||||
meta = self.doc.metadata
|
||||
key = key.split(".")
|
||||
for k in key[:-1]:
|
||||
meta = meta[k]
|
||||
del meta.content[key[-1]] # A hack because MetaMap doesn't have a __delitem__
|
||||
|
||||
|
||||
# This is a custom element which creates \begingroup \endgroup groups in TeX
|
||||
# and also causes KaTeX math blocks to be isolated in a similar way.
|
||||
#
|
||||
# Whenever a new context is created, its content should be eclosed in a group and vice-versa.
|
||||
class Group(Div):
|
||||
def __init__(self, *args, metadata={}, **kwargs):
|
||||
self.metadata = metadata
|
||||
super().__init__(*args, **kwargs)
|
62
formatitko.py
Executable file
62
formatitko.py
Executable file
|
@ -0,0 +1,62 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from typing import List
|
||||
import os
|
||||
|
||||
# Import local files
|
||||
from transform import transform
|
||||
from util import *
|
||||
from context import Context, Group
|
||||
from katex import KatexClient
|
||||
from html import html
|
||||
from tex import tex
|
||||
from images import ImageProcessor
|
||||
|
||||
from mj_show import show
|
||||
|
||||
# Initialize command line arguments
|
||||
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
parser.add_argument("-l", "--img-lookup-dirs", help="Image lookup directories. When processing images, the program will try to find the image in them first. Always looks for images in the same folder as the markdown file.", nargs="+", default=[])
|
||||
parser.add_argument("-p", "--img-public-dir", help="Directory to put processed images into. The program will not overwrite existing images.", default="public")
|
||||
parser.add_argument("-i", "--img-web-path", help="Path where the processed images are available on the website.", default="/")
|
||||
parser.add_argument("-w", "--output-html", help="The HTML file (for Web) to write into.", default="output.html")
|
||||
parser.add_argument("-t", "--output-tex", help="The TEX file to write into.", default="output.tex")
|
||||
parser.add_argument("input_filename", help="The markdown file to process.")
|
||||
parser.add_argument("--debug", action='store_true')
|
||||
args = parser.parse_args()
|
||||
# TODO: Accept path to unix socket for katexClient, then don't init our own,
|
||||
# just connect to an existing one. For formátíking many files in a row.
|
||||
|
||||
# Use panflute to parse the input MD file
|
||||
doc = import_md(open(args.input_filename, "r").read())
|
||||
|
||||
if args.debug:
|
||||
print(show(doc))
|
||||
|
||||
# The language metadatum is important, so it's read before transformation and
|
||||
# then attached to a group inside the Doc
|
||||
language = doc.get_metadata("language", None, True)
|
||||
context = Context(doc, args.input_filename)
|
||||
|
||||
# Transform the document. This includes all the fancy formatting this software does.
|
||||
doc = doc.walk(transform, context)
|
||||
|
||||
# Now wrap the document contents in a group, which is able to pop its language
|
||||
# setting out to TeX
|
||||
doc.content = [Group(*doc.content, metadata={"language":language})]
|
||||
|
||||
# Initialize the image processor (this just keeps some basic state)
|
||||
imageProcessor = ImageProcessor(args.img_public_dir, args.img_web_path, *args.img_lookup_dirs)
|
||||
|
||||
# Initialize KaTeX client (this runs the node app and connects to a unix socket)
|
||||
with KatexClient() as katexClient:
|
||||
# Generate HTML and TeX out of the transformed document
|
||||
open(args.output_html, "w").write(html(doc, katexClient, imageProcessor))
|
||||
open(args.output_tex, "w").write(tex(doc, imageProcessor))
|
||||
|
||||
if args.debug:
|
||||
print(show(doc))
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
\input luatex85.sty
|
||||
\input ucwmac2.tex
|
||||
\parskip=5pt plus 3pt minus 2pt
|
||||
\ucwmodule{luaofs}
|
||||
\ucwmodule{link}
|
||||
\ucwmodule{verb}
|
||||
\parskip=3pt plus 2pt minus 1pt
|
||||
\parindent=0sp
|
||||
|
||||
\def\strong#1{{%
|
||||
|
@ -14,17 +17,23 @@
|
|||
|
||||
|
||||
\def\superscript#1{\leavevmode\raise3pt\hbox{\fiverm#1}}
|
||||
\def\subscript#1{\leavevmode\lower1pt\hbox{\fiverm#1}}
|
||||
\newcount\fncount
|
||||
\fncount=1
|
||||
\def\fnmark{\superscript{\the\fncount}}
|
||||
\def\fn#1{\footnote\fnmark{#1}\advance\fncount by 1}
|
||||
\def\section#1{{\parskip1em\settextsize{18}\bf #1}}
|
||||
\def\subsection#1{{\parskip1em\settextsize{16}\bf #1}}
|
||||
\def\subsubsection#1{{\parskip1em\settextsize{14}\bf #1}}
|
||||
\def\subsubsubsection#1{{\parskip1em\settextsize{12}\bf #1}}
|
||||
\def\subsubsubsubsection#1{{\parskip1em\settextsize{10}\bf #1}}
|
||||
\def\subsubsubsubsubsection#1{{\parskip1em\settextsize{10}\bi #1}}
|
||||
\def\hA#1{{\parskip1em\settextsize{14}\bf #1}}
|
||||
\def\hB#1{{\parskip1em\settextsize{12}\bf #1}}
|
||||
\def\hC#1{{\parskip1em\settextsize{10}\bf #1}}
|
||||
\def\hD#1{{\parskip1em\settextsize{10}\bi #1}}
|
||||
\def\hr{{\vskip5pt\hrule\vskip5pt}}
|
||||
\long\def\blockquote#1{\vskip\lineskip\vskip\parskip\hbox{\vrule\hskip5pt\vbox{#1}}}
|
||||
\let\code\verbatim
|
||||
\let\codeblock\verbatim
|
||||
\def\subscript#1{\leavevmode\lower1pt\hbox{\fiverm#1}}
|
||||
\def\strikeout#1{FIXME: Strikeout not implemented}
|
||||
\def\underline#1{FIXME: Underline not implemented}
|
||||
\def\figure#1#2{\vskip5pt\centerline{#1}\centerline{#2}\vskip5pt}
|
||||
\def\figcaption#1{{\it #1}}
|
||||
\let\image\putimage
|
||||
\def\languagecs{} % KSP should define this to \cze probably
|
||||
\def\languageen{} % KSP should define this to \eng probably
|
||||
|
|
305
html.py
Normal file
305
html.py
Normal file
|
@ -0,0 +1,305 @@
|
|||
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
|
||||
|
||||
|
80
images.py
Normal file
80
images.py
Normal file
|
@ -0,0 +1,80 @@
|
|||
from typing import List
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from PIL import Image
|
||||
|
||||
class ImageProcessor:
|
||||
def __init__(self, public_dir: str, web_path: str, *lookup_dirs: List[str]):
|
||||
self.public_dir = public_dir
|
||||
self.lookup_dirs = lookup_dirs
|
||||
self.web_path = web_path if web_path[-1] != "/" else web_path[:-1]
|
||||
if not os.path.exists(self.public_dir):
|
||||
os.mkdir(self.public_dir)
|
||||
|
||||
def process_image(self, input_filename: str, format: str, source_dir: str, relative: bool=True, width: int=None, height:int=None, quality: int=None, dpi: int=None, fit: bool=True) -> str:
|
||||
name = os.path.basename(input_filename)
|
||||
base, ext = os.path.splitext(name)
|
||||
ext = ext[1:]
|
||||
full_path = self.find_image(input_filename, [source_dir])
|
||||
if full_path is None:
|
||||
raise FileNotFoundError(f'Image {input_filename} not found.')
|
||||
|
||||
# Generate filename from arguments
|
||||
suffix = ""
|
||||
geometry = None
|
||||
if width is not None or height is not None:
|
||||
geometry = f'{width if width is not None else ""}x{height if height is not None else ""}{"" if fit else "!"}'
|
||||
suffix += "_"+geometry
|
||||
if quality is not None:
|
||||
suffix += f'_q{quality}'
|
||||
target_name = base+suffix+"."+format
|
||||
target_path = self.public_dir + "/" + target_name
|
||||
|
||||
# Only regenerate if the file doesn't already exist.
|
||||
if not os.path.isfile(target_path):
|
||||
|
||||
# If the format is the same or it is just a different extension for
|
||||
# the same format, just copy it.
|
||||
if (((ext == format)
|
||||
or (ext == "epdf" and format == "pdf")
|
||||
or (ext == "jpg" and format == "jpeg"))
|
||||
and width is None and height is None and quality is None and dpi is None):
|
||||
shutil.copyfile(full_path, target_path)
|
||||
|
||||
# Try to find the converted filename in lookup_dirs, if you find
|
||||
# it, don't convert, just copy.
|
||||
elif self.find_image(target_name, [source_dir]):
|
||||
shutil.copyfile(self.find_image(target_name, [source_dir]), target_path)
|
||||
|
||||
# Convert SVGs using inkscape
|
||||
elif ext == "svg":
|
||||
width_arg = ['--export-width', str(width)] if width is not None else []
|
||||
height_arg = ['--export-height', str(height)] if height is not None else []
|
||||
dpi_arg = ['--export-dpi', str(dpi)] if dpi is not None else []
|
||||
if subprocess.run(['inkscape', full_path, '-o', target_path, *width_arg, *height_arg, *dpi_arg]).returncode != 0:
|
||||
raise Exception(f"Could not convert '{full_path}' to '{format}'")
|
||||
|
||||
# Convert everything else using ImageMagick.
|
||||
else:
|
||||
resize_arg = ['-resize', str(geometry)] if geometry is not None else []
|
||||
density_arg = ['-density', str(dpi)] if dpi is not None else []
|
||||
quality_arg = ['-quality', str(quality)] if quality is not None else []
|
||||
if subprocess.run(['convert', *density_arg, full_path, *resize_arg, *quality_arg, target_path]).returncode != 0:
|
||||
raise Exception(f"Could not convert '{full_path}' to '{format}'")
|
||||
|
||||
return target_name if relative else target_path
|
||||
|
||||
|
||||
def get_image_size(self, input_filename: str, additional_dirs: List[str]=[]) -> (int, int):
|
||||
full_path = self.find_image(input_filename, additional_dirs)
|
||||
if full_path is None:
|
||||
raise FileNotFoundError(f'Image {input_filename} not found.')
|
||||
# Getting image size using ImageMagick is slow. VERY
|
||||
return Image.open(full_path).size
|
||||
|
||||
|
||||
def find_image(self, input_filename: str, additional_dirs: List[str]=[]) -> str:
|
||||
for dir in [*self.lookup_dirs, *additional_dirs]:
|
||||
if os.path.isfile(dir + "/" + input_filename):
|
||||
return dir + "/" + input_filename
|
1
katex-server/.gitignore
vendored
Normal file
1
katex-server/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
node_modules
|
1
katex-server/README.md
Normal file
1
katex-server/README.md
Normal file
|
@ -0,0 +1 @@
|
|||
This was made by Standa Lukeš @exyi
|
1
katex-server/index.js
Normal file
1
katex-server/index.js
Normal file
|
@ -0,0 +1 @@
|
|||
console.log(require('katex').renderToString('\\frac{2a}{b}'))
|
127
katex-server/index.mjs
Normal file
127
katex-server/index.mjs
Normal file
|
@ -0,0 +1,127 @@
|
|||
// 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);
|
||||
|
||||
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.
|
||||
*/
|
||||
const 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
|
||||
}
|
||||
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')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
21
katex-server/package-lock.json
generated
Normal file
21
katex-server/package-lock.json
generated
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "ksp-katex-server",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"commander": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
|
||||
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="
|
||||
},
|
||||
"katex": {
|
||||
"version": "0.16.3",
|
||||
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.3.tgz",
|
||||
"integrity": "sha512-3EykQddareoRmbtNiNEDgl3IGjryyrp2eg/25fHDEnlHymIDi33bptkMv6K4EOC2LZCybLW/ZkEo6Le+EM9pmA==",
|
||||
"requires": {
|
||||
"commander": "^8.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
14
katex-server/package.json
Normal file
14
katex-server/package.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
64
katex.py
Normal file
64
katex.py
Normal file
|
@ -0,0 +1,64 @@
|
|||
import socket
|
||||
import subprocess
|
||||
import tempfile
|
||||
import json
|
||||
import os
|
||||
from typing import Dict
|
||||
import time
|
||||
|
||||
|
||||
class KatexError(Exception):
|
||||
pass
|
||||
|
||||
class KatexClient:
|
||||
def __init__(self):
|
||||
# Create temporary directory for socket
|
||||
self._temp_dir = tempfile.TemporaryDirectory(prefix='formatitko')
|
||||
self._socket_file = self._temp_dir.name + "/katex-socket"
|
||||
|
||||
self._server_process = subprocess.Popen(["node", os.path.dirname(os.path.realpath(__file__)) + "/katex-server/index.mjs", self._socket_file])
|
||||
|
||||
self._client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
|
||||
# Wait for the node program to create the socket file
|
||||
while not os.path.exists(self._socket_file):
|
||||
time.sleep(0.01)
|
||||
|
||||
# Wait for the node program to start accepting connections
|
||||
while True:
|
||||
try:
|
||||
self._client.connect(self._socket_file)
|
||||
time.sleep(0.01)
|
||||
except ConnectionRefusedError:
|
||||
continue
|
||||
break
|
||||
|
||||
def render(self, tex: str, options: Dict={}):
|
||||
# Send formulas to translate
|
||||
self._client.sendall((json.dumps({"formulas":[{"tex":tex}], "options":options})+"\n").encode("utf-8"))
|
||||
|
||||
# Receive response
|
||||
data = self._client.recv(4096)
|
||||
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"]
|
||||
|
||||
# Special commands implemented in the JS file for grouping defs together.
|
||||
def begingroup(self):
|
||||
self._client.sendall("begingroup\n".encode("utf-8"))
|
||||
|
||||
def endgroup(self):
|
||||
self._client.sendall("endgroup\n".encode("utf-8"))
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, type, value, tb):
|
||||
self._server_process.terminate()
|
|
@ -1,65 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
# Import local files
|
||||
from formatitko.transform import transform
|
||||
from formatitko.util import import_md
|
||||
from formatitko.context import Context, Group
|
||||
from formatitko.katex import KatexClient
|
||||
from formatitko.images import ImageProcessor
|
||||
from ksp_html_generator import KSPHTMLGenerator
|
||||
|
||||
from formatitko.mj_show import show
|
||||
|
||||
def main():
|
||||
# Initialize command line arguments
|
||||
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
parser.add_argument("-l", "--img-lookup-dirs", help="Image lookup directories. When processing images, the program will try to find the image in them first. Always looks for images in the same folder as the markdown file.", nargs="+", default=[])
|
||||
parser.add_argument("-p", "--img-public-dir", help="Directory to put processed images into. The program will overwrite images, whose dependencies are newer.", default="public")
|
||||
parser.add_argument("-c", "--img-cache-dir", help="Directory to cache processed images and intermediate products. The program will overwrite files, whose dependencies are newer.", default="cache")
|
||||
parser.add_argument("-i", "--img-web-path", help="Path where the processed images are available on the website.", default="/")
|
||||
parser.add_argument("-w", "--output-html", help="The HTML file (for Web) to write into.", default="output.html")
|
||||
parser.add_argument("-t", "--output-tex", help="The TEX file to write into.", default="output.tex")
|
||||
parser.add_argument("input_filename", help="The markdown file to process.")
|
||||
parser.add_argument("--debug", action='store_true')
|
||||
args = parser.parse_args()
|
||||
# TODO: Accept path to unix socket for katexClient, then don't init our own,
|
||||
# just connect to an existing one. For formátíking many files in a row.
|
||||
|
||||
# Use panflute to parse the input MD file
|
||||
doc = import_md(open(args.input_filename, "r").read())
|
||||
|
||||
if args.debug:
|
||||
print(show(doc))
|
||||
|
||||
# The language metadatum is important, so it's read before transformation and
|
||||
# then attached to a group inside the Doc
|
||||
language = doc.get_metadata("language", None, True)
|
||||
context = Context(doc, args.input_filename)
|
||||
|
||||
# Transform the document. This includes all the fancy formatting this software does.
|
||||
doc = doc.walk(transform, context)
|
||||
|
||||
# Now wrap the document contents in a group, which is able to pop its language
|
||||
# setting out to TeX
|
||||
doc.content = [Group(*doc.content, metadata={"language":language})]
|
||||
|
||||
# Initialize the image processor (this just keeps some basic state)
|
||||
imageProcessor = ImageProcessor(args.img_public_dir, args.img_web_path, args.img_cache_dir, *args.img_lookup_dirs)
|
||||
|
||||
# Initialize KaTeX client (this runs the node app and connects to a unix socket)
|
||||
with KatexClient() as katexClient:
|
||||
# Generate HTML and TeX out of the transformed document
|
||||
#open(args.output_html, "w").write(html(doc, katexClient, imageProcessor))
|
||||
#open(args.output_tex, "w").write(tex(doc, imageProcessor))
|
||||
KSPHTMLGenerator(sys.stdout, katexClient, imageProcessor).generate(doc)
|
||||
|
||||
if args.debug:
|
||||
print(show(doc))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
# A sample on how a custom html generator might look
|
||||
|
||||
from formatitko.html_generator import HTMLGenerator
|
||||
|
||||
from panflute import Link
|
||||
|
||||
class KSPHTMLGenerator(HTMLGenerator):
|
||||
def generate_Link(self, e: Link):
|
||||
if e.url.startswith("ksp://"):
|
||||
e.url = "https://ksp.mff.cuni.cz/viz/" + e.url[len("ksp://"):]
|
||||
return super().generate_Link(e)
|
|
@ -3,7 +3,7 @@
|
|||
###
|
||||
|
||||
import sys, re
|
||||
from panflute import Str, Element, ListContainer, DictContainer
|
||||
from panflute import *
|
||||
|
||||
avoid_keys = {
|
||||
'dict',
|
|
@ -1,43 +0,0 @@
|
|||
[build-system]
|
||||
requires = ["setuptools>=61.0","setuptools_scm[toml]>=6.2"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "formatitko"
|
||||
#version = "0.0.1"
|
||||
dynamic = ["version"]
|
||||
authors = [
|
||||
{ name="Greenscreener", email="gs@grsc.cz" },
|
||||
{ name="Organizers of KSP", email="ksp@mff.cuni.cz" }
|
||||
]
|
||||
description = "A python program based on pandoc and its python library panflute for converting from markdown to TeX and HTML with added fancy features like image processing, python-based macros and much more."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.9"
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: Linux",
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
"Pygments>=2.14.0",
|
||||
"panflute>=2.3.0",
|
||||
"fontTools>=4.38.0",
|
||||
"Pillow>=9.4.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
"Homepage" = "https://gitea.ks.matfyz.cz/KSP/formatitko"
|
||||
"Bug Tracker" = "https://gitea.ks.matfyz.cz/KSP/formatitko/issues"
|
||||
|
||||
[project.scripts]
|
||||
formatitko = "formatitko.formatitko:main"
|
||||
|
||||
[tool.setuptools_scm]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
exclude = ["src/formatitko/katex-server/node_modules"]
|
||||
|
||||
[tool.pyright]
|
||||
strictParameterNoneValue = false
|
|
@ -1,4 +0,0 @@
|
|||
from formatitko.formatitko import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -1,34 +0,0 @@
|
|||
from panflute import Span, Div, Element, Plain, Para
|
||||
|
||||
class InlineError(Exception):
|
||||
pass
|
||||
|
||||
class Command:
|
||||
pass
|
||||
|
||||
# This distinction is needed because while transforming the tree, inline
|
||||
# elements cannot be replaced with block ones
|
||||
class InlineCommand(Span, Command):
|
||||
def replaceSelf(self, *content: list[Element]) -> Span:
|
||||
try:
|
||||
return Span(*content)
|
||||
except TypeError:
|
||||
if len(content) == 1 and (isinstance(content[0], Para) or isinstance(content[0], Plain)):
|
||||
return Span(*content[0].content)
|
||||
else:
|
||||
return Div(*content)
|
||||
pass
|
||||
|
||||
class BlockCommand(Div, Command):
|
||||
def replaceSelf(self, *content: list[Element]) -> Div:
|
||||
try:
|
||||
return Div(*content)
|
||||
except TypeError:
|
||||
return Div(Para(*content))
|
||||
pass
|
||||
|
||||
class CodeCommand(BlockCommand):
|
||||
test: str
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.text = args[0]
|
||||
super().__init__(**kwargs)
|
|
@ -1,9 +0,0 @@
|
|||
import panflute as pf
|
||||
import formatitko.elements as fe
|
||||
from formatitko.util import import_md_list
|
||||
from formatitko.util import parse_string
|
||||
|
||||
from formatitko.context import Context
|
||||
from formatitko.command import Command
|
||||
from .nop_processor import NOPProcessor
|
||||
from panflute import Element
|
|
@ -1,34 +0,0 @@
|
|||
from .context import Context, CommandCallable # This is there because of a wild circular import dependency between many functions and classes
|
||||
from panflute import CodeBlock, Null
|
||||
|
||||
|
||||
from . import command_env
|
||||
from .util import nullify
|
||||
|
||||
def parse_command(code: str) -> CommandCallable:
|
||||
code_lines = code.split("\n")
|
||||
tabs = False
|
||||
for line in code_lines:
|
||||
if len(line) != 0 and line[0] == "\t":
|
||||
tabs = True
|
||||
break
|
||||
indented_code_lines = []
|
||||
for line in code_lines:
|
||||
indented_code_lines.append(("\t" if tabs else " ")+line)
|
||||
code = "def command(element: Command, context: Context, processor: NOPProcessor) -> list[Element]:\n"+"\n".join(indented_code_lines)
|
||||
env = {**command_env.__dict__}
|
||||
exec(code, env)
|
||||
return env["command"]
|
||||
|
||||
# This function is called in trasform.py, defining a command which can be
|
||||
# called later
|
||||
def handle_command_define(e: CodeBlock, c: Context) -> Null:
|
||||
command = parse_command(e.text)
|
||||
if "define" in e.attributes:
|
||||
if not c.get_command(e.attributes["define"]):
|
||||
c.set_command(e.attributes["define"], command)
|
||||
else:
|
||||
raise NameError(f"Command already defined: '{e.attributes['define']}'")
|
||||
if "redefine" in e.attributes:
|
||||
c.set_command(e.attributes["redefine"], command)
|
||||
return nullify(e)
|
|
@ -1,188 +0,0 @@
|
|||
from panflute import Doc, Element, Div, Span
|
||||
|
||||
from typing import Union, Callable
|
||||
from types import ModuleType
|
||||
import os
|
||||
|
||||
from .command import Command
|
||||
|
||||
CommandCallable = Callable[[Command, 'Context', 'NOPProcessor'], list[Element]] # This is here because of a wild circular import dependency between many functions and classes
|
||||
|
||||
# This class is used to keep state while transforming the document using
|
||||
# transform.py. For the context to be available to the html and TeX generators,
|
||||
# individual keys must be manually assigned to the individual elements. This is
|
||||
# done in transform.py.
|
||||
#
|
||||
# The context is also aware of its parent contexts and relevant data (such as
|
||||
# metadata and commands) can be read from the closest parent context. Writing
|
||||
# only happens to the current one.
|
||||
#
|
||||
# This class is basically an extension to panflute's doc, this is why metadata
|
||||
# is read directly from it.
|
||||
class Context:
|
||||
parent: Union["Context", None]
|
||||
_commands: dict[str, Union[CommandCallable, None]]
|
||||
_data: dict[str, object]
|
||||
doc: Doc
|
||||
trusted: bool
|
||||
path: str
|
||||
dir: str
|
||||
filename: str
|
||||
root_dir: str # Absolute path to the dir of the file formátítko was called on
|
||||
rel_dir: str # Relative path to the current dir from the root dir
|
||||
deps: set[str]
|
||||
|
||||
def __init__(self, doc: Doc, path: str, parent: Union['Context', None]=None, trusted: bool=True):
|
||||
self.parent = parent
|
||||
self._commands = {}
|
||||
self._data = {}
|
||||
self.doc = doc
|
||||
self.trusted = trusted
|
||||
self.path = path
|
||||
self.dir = os.path.dirname(path) if os.path.dirname(path) != "" else "."
|
||||
self.filename = os.path.basename(path)
|
||||
self.root_dir = parent.root_dir if parent else os.path.abspath(self.dir)
|
||||
self.rel_dir = os.path.relpath(self.dir, self.root_dir)
|
||||
self.deps = set()
|
||||
self.add_dep(path)
|
||||
if self.get_metadata("flags", immediate=True) is None:
|
||||
self.set_metadata("flags", {})
|
||||
|
||||
def get_command(self, command: str) -> Union[CommandCallable, None]:
|
||||
if command in self._commands:
|
||||
return self._commands[command]
|
||||
elif self.parent:
|
||||
return self.parent.get_command(command)
|
||||
else:
|
||||
return None
|
||||
|
||||
def set_command(self, command: str, val: CommandCallable):
|
||||
self._commands[command] = val
|
||||
|
||||
def unset_command(self, command: str):
|
||||
del self._commands[command]
|
||||
|
||||
def add_commands_from_module(self, module: Union[dict[str, CommandCallable], ModuleType], module_name: str=""):
|
||||
if isinstance(module, ModuleType):
|
||||
module = module.__dict__
|
||||
prefix = module_name+"." if module_name else ""
|
||||
for name, func in module.items():
|
||||
if isinstance(func, Callable):
|
||||
self.set_command(prefix+name, func)
|
||||
|
||||
def is_flag_set(self, flag: str):
|
||||
if self.get_metadata("flags."+flag) is not None:
|
||||
if self.get_metadata("flags."+flag):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
elif self.parent:
|
||||
return self.parent.is_flag_set(flag)
|
||||
else:
|
||||
return False
|
||||
|
||||
def set_flag(self, flag: str, val: bool):
|
||||
self.set_metadata("flags."+flag, val)
|
||||
|
||||
def unset_flag(self, flag: str):
|
||||
self.unset_metadata("flags."+flag)
|
||||
|
||||
def get_metadata(self, key: str, simple: bool=True, immediate: bool=False):
|
||||
value = self.doc.get_metadata(key, None, simple)
|
||||
if value is not None:
|
||||
return value
|
||||
elif self.parent and not immediate:
|
||||
return self.parent.get_metadata(key)
|
||||
else:
|
||||
return None
|
||||
|
||||
def set_metadata(self, key: str, value):
|
||||
meta = self.doc.metadata
|
||||
keys = key.split(".")
|
||||
for k in keys[:-1]:
|
||||
meta = meta[k]
|
||||
meta[keys[-1]] = value
|
||||
|
||||
def unset_metadata(self, key: str):
|
||||
meta = self.doc.metadata
|
||||
keys = key.split(".")
|
||||
for k in keys[:-1]:
|
||||
meta = meta[k]
|
||||
del meta.content[key[-1]] # A hack because MetaMap doesn't have a __delitem__
|
||||
|
||||
def import_metadata(self, data, key: str=""):
|
||||
if isinstance(data, dict) and isinstance(self.get_metadata(key), dict):
|
||||
for subkey, value in enumerate(data):
|
||||
self.import_metadata(value, key+"."+subkey if key != "" else subkey)
|
||||
else:
|
||||
self.set_metadata(key, data)
|
||||
|
||||
def get_data(self, key: str, immediate: bool=False):
|
||||
data = self._data
|
||||
keys = key.split(".")
|
||||
try:
|
||||
for k in keys:
|
||||
data = data[k]
|
||||
return data
|
||||
except KeyError:
|
||||
if self.parent and not immediate:
|
||||
return self.parent.get_data(key)
|
||||
else:
|
||||
return None
|
||||
|
||||
def set_data(self, key: str, value: object):
|
||||
data = self._data
|
||||
keys = key.split(".")
|
||||
for k in keys[:-1]:
|
||||
try:
|
||||
data = data[k]
|
||||
except KeyError:
|
||||
data[k] = {}
|
||||
data = data[k]
|
||||
data[keys[-1]] = value
|
||||
|
||||
def unset_data(self, key: str):
|
||||
if key == "":
|
||||
self._data = {}
|
||||
data = self._data
|
||||
keys = key.split(".")
|
||||
for k in keys[:-1]:
|
||||
data = data[k]
|
||||
del data[keys[-1]]
|
||||
|
||||
def get_deps(self) -> list[str]:
|
||||
if self.parent is not None:
|
||||
return self.parent.get_deps()
|
||||
else:
|
||||
return self.deps
|
||||
|
||||
def add_dep(self, dep: str):
|
||||
self.get_deps().add(os.path.abspath(dep))
|
||||
|
||||
def add_deps(self, deps: list[str]):
|
||||
self.get_deps().update([os.path.abspath(path) for path in deps])
|
||||
|
||||
|
||||
def get_context_from_doc(doc: Doc) -> Context:
|
||||
if len(doc.content) == 1 and isinstance(doc.content[0], Group):
|
||||
return doc.content[0].context
|
||||
else:
|
||||
return None
|
||||
|
||||
# This is a custom element which creates \begingroup \endgroup groups in TeX
|
||||
# and also causes KaTeX math blocks to be isolated in a similar way.
|
||||
#
|
||||
# Whenever a new context is created, its content should be eclosed in a group and vice-versa.
|
||||
class Group(Element):
|
||||
metadata: dict
|
||||
context: Context
|
||||
def __init__(self, *args, context:Context, metadata={}, **kwargs):
|
||||
self.metadata = metadata # This is only here for backwards compatibility with old html.py, tex.py and transform.py. FIXME: Remove this when the time comes.
|
||||
self.context = context
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
class BlockGroup(Group, Div):
|
||||
pass
|
||||
|
||||
class InlineGroup(Group, Span):
|
||||
pass
|
|
@ -1,19 +0,0 @@
|
|||
from panflute import Quoted, Link
|
||||
|
||||
|
||||
from .command import Command, InlineCommand, BlockCommand, CodeCommand
|
||||
from .context import Group, BlockGroup, InlineGroup
|
||||
from .whitespace import Whitespace, NBSP
|
||||
|
||||
# This is a small extension to the Quoted panflute elements which allows to
|
||||
# have language-aware quotation marks.
|
||||
class FQuoted(Quoted):
|
||||
style: str
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.style = kwargs["style"]
|
||||
del kwargs["style"]
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class FileLink(Link):
|
||||
pass
|
|
@ -1,133 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import tempfile
|
||||
import subprocess
|
||||
import shutil
|
||||
|
||||
# Import local files
|
||||
from .util import import_md
|
||||
from .katex import KatexClient
|
||||
from .images import ImageProcessor, ImageProcessorNamespace
|
||||
from .output_generator import OutputGenerator, FormatitkoRecursiveError
|
||||
from .html_generator import HTMLGenerator, StandaloneHTMLGenerator
|
||||
from .transform_processor import TransformProcessor
|
||||
from .pandoc_processor import PandocProcessor
|
||||
from .tex_generator import UCWTexGenerator
|
||||
from .context import get_context_from_doc
|
||||
|
||||
from panflute import convert_text
|
||||
|
||||
def main():
|
||||
# Initialize command line arguments
|
||||
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
parser.add_argument("-l", "--img-lookup-dirs", help="Image lookup directories. When processing images, the program will try to find the image in them first. Always looks for images in the same folder as the markdown file.", nargs="+", default=[])
|
||||
parser.add_argument("-p", "--img-public-dir", help="Directory to put processed images into. The program will overwrite images, whose dependencies are newer.", default="public")
|
||||
parser.add_argument("-c", "--img-cache-dir", help="Directory to cache processed images and intermediate products. The program will overwrite files, whose dependencies are newer.", default="cache")
|
||||
parser.add_argument("-i", "--img-web-path", help="Path where the processed images are available on the website.", default="/")
|
||||
parser.add_argument("-w", "--output-html", help="The HTML file (for Web) to write into.")
|
||||
parser.add_argument("-s", "--output-standalone-html", help="The Standalone HTML file to write into. A full page is generated instead of just a fragment.")
|
||||
parser.add_argument("-t", "--output-tex", help="The TEX file to write into.")
|
||||
parser.add_argument("-m", "--output-md", help="The Markdown file to write into. (Uses pandoc to generate markdown)")
|
||||
parser.add_argument("-j", "--output-json", help="The JSON file to dump the pandoc-compatible AST into.")
|
||||
parser.add_argument("-P", "--output-pdf", help="The PDF file to save the PDF generated by TeX.")
|
||||
parser.add_argument("--katex-server", action='store_true', help="Starts a KaTeX server and prints the socket filename onto stdout. Useful for running formatitko many times without starting the KaTeX server each time.")
|
||||
parser.add_argument("-k", "--katex-socket", help="The KaTeX server socket filename obtained by running with `--katex-server`.")
|
||||
parser.add_argument("input_filename", help="The markdown file to process.", nargs="?" if "--katex-server" in sys.argv else None)
|
||||
parser.add_argument("--debug", action='store_true')
|
||||
parser.add_argument("--traceback-limit", help="Traceback limit for when errors happen, defaults to 0, as it is only useful for internal debugging.", default=0)
|
||||
parser.add_argument("--deps", help="File to write list of dependencies to. May depend on output formats used.")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.katex_server:
|
||||
with KatexClient(connect=False) as katexClient:
|
||||
print(katexClient.get_socket())
|
||||
try:
|
||||
while True:
|
||||
pass
|
||||
except KeyboardInterrupt:
|
||||
print("Exiting...")
|
||||
return
|
||||
|
||||
# Use panflute to parse the input MD file
|
||||
doc = import_md(open(args.input_filename, "r").read())
|
||||
|
||||
if args.debug:
|
||||
try:
|
||||
OutputGenerator(sys.stdout).generate(doc)
|
||||
except FormatitkoRecursiveError as e:
|
||||
e.pretty_print(tracebacklimit=args.traceback_limit)
|
||||
|
||||
try:
|
||||
doc = TransformProcessor(args.input_filename).transform(doc)
|
||||
except FormatitkoRecursiveError as e:
|
||||
e.pretty_print(tracebacklimit=args.traceback_limit)
|
||||
|
||||
# Initialize the image processor (this just keeps some basic state)
|
||||
imageProcessor = ImageProcessor({"": ImageProcessorNamespace(args.img_public_dir, args.img_web_path, args.img_cache_dir, args.img_lookup_dirs, True)})
|
||||
|
||||
if args.output_html is not None:
|
||||
# Initialize KaTeX client (this runs the node app and connects to a unix socket)
|
||||
with KatexClient(socket=args.katex_socket) as katexClient:
|
||||
with open(args.output_html, "w") as file:
|
||||
try:
|
||||
HTMLGenerator(file, katexClient, imageProcessor).generate(doc)
|
||||
except FormatitkoRecursiveError as e:
|
||||
e.pretty_print(tracebacklimit=args.traceback_limit)
|
||||
|
||||
if args.output_standalone_html is not None:
|
||||
# Initialize KaTeX client (this runs the node app and connects to a unix socket)
|
||||
with KatexClient(socket=args.katex_socket) as katexClient:
|
||||
with open(args.output_standalone_html, "w") as file:
|
||||
try:
|
||||
StandaloneHTMLGenerator(file, katexClient, imageProcessor).generate(doc)
|
||||
except FormatitkoRecursiveError as e:
|
||||
e.pretty_print(tracebacklimit=args.traceback_limit)
|
||||
|
||||
if args.output_tex is not None:
|
||||
with open(args.output_tex, "w") as file:
|
||||
try:
|
||||
UCWTexGenerator(file, imageProcessor).generate(doc)
|
||||
except FormatitkoRecursiveError as e:
|
||||
e.pretty_print(tracebacklimit=args.traceback_limit)
|
||||
|
||||
if args.output_md is not None:
|
||||
with open(args.output_md, "w") as file:
|
||||
file.write(convert_text(PandocProcessor().transform(doc), input_format="panflute", output_format="markdown"))
|
||||
|
||||
if args.output_json is not None:
|
||||
with open(args.output_json, "w") as file:
|
||||
file.write(convert_text(PandocProcessor().transform(doc), input_format="panflute", output_format="json"))
|
||||
|
||||
if args.output_pdf is not None:
|
||||
if args.output_tex is None:
|
||||
fd = tempfile.NamedTemporaryFile(dir=".", suffix=".tex")
|
||||
with open(fd.name, "w") as file:
|
||||
try:
|
||||
UCWTexGenerator(file, imageProcessor).generate(doc)
|
||||
except FormatitkoRecursiveError as e:
|
||||
e.pretty_print(tracebacklimit=args.traceback_limit)
|
||||
filename = fd.name
|
||||
else:
|
||||
filename = args.output_tex
|
||||
outdir = tempfile.TemporaryDirectory(prefix="formatitko")
|
||||
subprocess.run(["pdfcsplain", "-halt-on-error", "-output-directory="+outdir.name, "-jobname=formatitko", filename], check=True)
|
||||
shutil.move(outdir.name+"/formatitko.pdf", args.output_pdf)
|
||||
|
||||
if args.deps is not None:
|
||||
with open(args.deps, "w") as file:
|
||||
for dep in get_context_from_doc(doc).get_deps():
|
||||
file.write(dep + "\n")
|
||||
|
||||
if args.debug:
|
||||
print("-----------------------------------")
|
||||
try:
|
||||
OutputGenerator(sys.stdout).generate(doc)
|
||||
except FormatitkoRecursiveError as e:
|
||||
e.pretty_print(tracebacklimit=args.traceback_limit)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,376 +0,0 @@
|
|||
from panflute import Cite, Code, Emph, Image, LineBreak, Link, Math, Note, RawInline, SmallCaps, Str, Strikeout, Subscript, Superscript, Underline
|
||||
from panflute import BulletList, Citation, CodeBlock, Definition, DefinitionItem, DefinitionList, Header, HorizontalRule, LineBlock, LineItem, ListItem, Null, OrderedList, Para, Plain, RawBlock, TableBody, TableFoot, TableHead
|
||||
from panflute import TableRow, TableCell, Caption, Doc
|
||||
from panflute import ListContainer, Element
|
||||
from typing import Union
|
||||
|
||||
import os
|
||||
import io
|
||||
import warnings
|
||||
|
||||
from pygments import highlight
|
||||
from pygments.lexers import get_lexer_by_name
|
||||
from pygments.formatters import HtmlFormatter
|
||||
from pygments.util import ClassNotFound
|
||||
|
||||
from .whitespace import NBSP
|
||||
from .context import Group, BlockGroup, InlineGroup
|
||||
from .output_generator import OutputGenerator
|
||||
from .katex import KatexClient
|
||||
from .images import ImageProcessor, ImageProcessorNamespaceSearcher
|
||||
from .util import inlinify
|
||||
from .elements import FileLink
|
||||
|
||||
|
||||
class HTMLGenerator(OutputGenerator):
|
||||
imageProcessor: ImageProcessor
|
||||
katexClient: KatexClient
|
||||
|
||||
def __init__(self, output_file, katexClient: KatexClient, imageProcessor: ImageProcessor, *args, **kwargs):
|
||||
self.katexClient = katexClient
|
||||
self.imageProcessor = imageProcessor
|
||||
super().__init__(output_file, *args, **kwargs)
|
||||
|
||||
def generate(self, e: Union[Element, ListContainer]):
|
||||
if hasattr(e, "attributes") and "only" in e.attributes and e.attributes["only"] != "html":
|
||||
return
|
||||
super().generate(e)
|
||||
|
||||
def escape_special_chars(self, text: str) -> str:
|
||||
text = text.replace("&", "&")
|
||||
text = text.replace("<", "<")
|
||||
text = text.replace(">", ">")
|
||||
text = text.replace("\"", """)
|
||||
text = text.replace("'", "'")
|
||||
# text = text.replace(" ", " ") # Don't replace no-break spaces with HTML escapes, because we trust unicode?
|
||||
return text
|
||||
|
||||
def start_tag(self, tag: str, attributes: dict[str,str]={}) -> str:
|
||||
words = [tag]
|
||||
for key, value in attributes.items():
|
||||
if value is not None:
|
||||
words.append(f"{key}=\"{self.escape_special_chars(value)}\"")
|
||||
return "<" + " ".join(words) + ">"
|
||||
|
||||
def end_tag(self, tag: str, attributes: dict[str,str]={}) -> str:
|
||||
return "</" + tag + ">"
|
||||
|
||||
def single_tag(self, tag: str, attributes: dict[str,str]={}) -> str:
|
||||
return self.start_tag(tag, attributes)
|
||||
|
||||
def tagname(self, e) -> str:
|
||||
if isinstance(e, Header):
|
||||
return "h" + str(e.level)
|
||||
try:
|
||||
return {
|
||||
BulletList: "ul",
|
||||
Doc: "main",
|
||||
Emph: "em",
|
||||
Caption: "figcaption",
|
||||
Para: "p",
|
||||
LineBlock: "p",
|
||||
LineBreak: "br",
|
||||
Link: "a",
|
||||
ListItem: "li",
|
||||
HorizontalRule: "hr",
|
||||
OrderedList: "ol",
|
||||
SmallCaps: "span",
|
||||
Strikeout: "strike",
|
||||
Subscript: "sub",
|
||||
Superscript: "sup",
|
||||
Underline: "u",
|
||||
TableBody: "tbody",
|
||||
TableHead: "thead",
|
||||
TableFoot: "tfoot",
|
||||
TableRow: "tr",
|
||||
TableCell: "td",
|
||||
InlineGroup: "span",
|
||||
BlockGroup: "div"
|
||||
}[type(e)]
|
||||
except KeyError:
|
||||
return type(e).__name__.lower()
|
||||
|
||||
def common_attributes(self, e) -> dict[str,str]:
|
||||
attributes = {}
|
||||
if hasattr(e, "identifier") and e.identifier != "":
|
||||
attributes["id"] = e.identifier
|
||||
if hasattr(e, "classes") and len(e.classes) != 0:
|
||||
attributes["class"] = " ".join(e.classes)
|
||||
return attributes
|
||||
|
||||
def generate_NBSP(self, e: NBSP):
|
||||
self.write(" ") # Unicode no-break space, because we trust unicode?
|
||||
|
||||
def generate_Null(self, e: Null):
|
||||
pass
|
||||
|
||||
#def generate_Doc(self, e: Doc):
|
||||
# formatter = HtmlFormatter(style=e.get_metadata("highlight-style") if e.get_metadata("highlight-style") is not None else "default")
|
||||
# self.generate_simple_tag(tag="style", attributes={}, content=formatter.get_style_defs(".highlight"))
|
||||
#self.generate_simple_tag(e, tag="main")
|
||||
|
||||
def generate_CodeBlock(self, e: CodeBlock):
|
||||
lexer = None
|
||||
if e.classes and 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:
|
||||
warnings.warn(f"Syntax highligher does not have lexer for element with these classes: {e.classes}", UserWarning)
|
||||
|
||||
if lexer:
|
||||
formatter = HtmlFormatter(style=e.attributes["style"], noclasses=True)
|
||||
result = highlight(e.text, lexer, formatter)
|
||||
self.writeraw(result)
|
||||
else:
|
||||
e.text = self.escape_special_chars(e.text)
|
||||
self.generate_simple_tag(e, tag="pre")
|
||||
|
||||
def generate_Code(self, e: Code):
|
||||
e.text = self.escape_special_chars(e.text)
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_Image(self, e: Image):
|
||||
url = e.url
|
||||
|
||||
additional_args = self.get_image_processor_args(e.attributes)
|
||||
additional_args["context"] = self.context
|
||||
|
||||
# The directory of the current file relative to the current working directory
|
||||
source_dir = self.context.dir
|
||||
# The directory of the current file relative to the md file we were called on
|
||||
rel_dir = self.context.rel_dir
|
||||
|
||||
searcher = self.imageProcessor.get_searcher_by_path(url, rel_dir, source_dir)
|
||||
url = self.imageProcessor.get_path_without_namespace(url)
|
||||
|
||||
_, 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 = self.imageProcessor.process_image(url, ext, searcher, **additional_args)
|
||||
elif ext in ["pdf", "epdf","asy"]:
|
||||
# Only relevant for when these were PNGs, leaving this here for future reference.
|
||||
# if not "dpi" in additional_args:
|
||||
# additional_args["dpi"] = 300
|
||||
url = self.imageProcessor.process_image(url, "svg", searcher, **additional_args)
|
||||
elif ext in ["jpg"]:
|
||||
url = self.imageProcessor.process_image(url, "jpeg", searcher, **additional_args)
|
||||
else:
|
||||
url = self.imageProcessor.process_image(url, "png", searcher, **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 = self.imageProcessor.get_image_size(searcher.find_image_in_dir(url, searcher.get_cache_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'{searcher.get_web_path()}/{url}', f'{width}w'))
|
||||
break
|
||||
quality = size[2] if ext == "jpeg" else None
|
||||
cache_img = self.imageProcessor.process_image(url, ext, searcher.get_cache_searcher(), width=size[0], height=size[1], quality=quality)
|
||||
searcher.publish_image(cache_img)
|
||||
srcset.append((f'{searcher.get_web_path()}/{cache_img}', f'{size[0]}w'))
|
||||
|
||||
searcher.publish_image(url)
|
||||
url = searcher.get_web_path() + "/" + url
|
||||
|
||||
attributes = self.common_attributes(e)
|
||||
if "width" in e.attributes:
|
||||
attributes["width"] = e.attributes["width"]
|
||||
if "height" in e.attributes:
|
||||
attributes["height"] = e.attributes["height"]
|
||||
if "title" in e.attributes:
|
||||
attributes["title"] = e.attributes["title"]
|
||||
|
||||
if e.title:
|
||||
attributes["alt"] = e.title
|
||||
else:
|
||||
fake_out = io.StringIO()
|
||||
HTMLGenerator(fake_out, self.katexClient, self.imageProcessor).generate(e.content)
|
||||
attributes["alt"] = fake_out.getvalue()
|
||||
|
||||
if len(srcset) > 1:
|
||||
attributes["src"] = srcset[-1][0]
|
||||
attributes["srcset"] = ", ".join([" ".join(src) for src in srcset])
|
||||
else:
|
||||
attributes["src"] = url
|
||||
|
||||
if e.attributes["no-img-link"]:
|
||||
self.write(self.single_tag("img", attributes))
|
||||
return
|
||||
|
||||
img = RawInline(self.single_tag("img", attributes))
|
||||
link = Link(img, url=url)
|
||||
|
||||
self.generate(link)
|
||||
|
||||
def generate_FileLink(self, e: FileLink):
|
||||
url = e.url
|
||||
|
||||
# The directory of the current file relative to the current working directory
|
||||
source_dir = self.context.dir
|
||||
# The directory of the current file relative to the md file we were called on
|
||||
rel_dir = self.context.rel_dir
|
||||
|
||||
searcher = self.imageProcessor.get_searcher_by_path(url, rel_dir, source_dir)
|
||||
url = self.imageProcessor.get_path_without_namespace(url)
|
||||
|
||||
url = self.imageProcessor.process_image(url, "", searcher, self.context)
|
||||
searcher.publish_image(url)
|
||||
url = searcher.get_web_path() + "/" + url
|
||||
|
||||
self.generate_Link(Link(*e.content, url=url))
|
||||
|
||||
def generate_InlineGroup(self, e: InlineGroup):
|
||||
self.generate_Group(e)
|
||||
|
||||
def generate_BlockGroup(self, e: BlockGroup):
|
||||
self.generate_Group(e)
|
||||
|
||||
def generate_Group(self, e: Group):
|
||||
self.katexClient.begingroup()
|
||||
self.generate_simple_tag(e, attributes=self.common_attributes(e) | {"lang":self.context.get_metadata("lang")})
|
||||
self.katexClient.endgroup()
|
||||
|
||||
def generate_Plain(self, e: Plain):
|
||||
self.generate(e.content)
|
||||
|
||||
def generate_LineItem(self, e: LineItem):
|
||||
self.generate(e.content)
|
||||
self.write("<br>")
|
||||
self.endln()
|
||||
|
||||
# Footnotes are placed into parentheses. (And not footnotes (This is how KSP did it before))
|
||||
def generate_Note(self, e: Note):
|
||||
inline = inlinify(e)
|
||||
tag = self.tagname(e)
|
||||
if inline is not None:
|
||||
self.write(self.start_tag(tag)+" (")
|
||||
self.generate(inline)
|
||||
self.write(") "+self.end_tag(tag))
|
||||
else:
|
||||
self.writeln(self.start_tag(tag) + "(")
|
||||
self.indent_more()
|
||||
self.generate(e.content)
|
||||
self.indent_less()
|
||||
self.writeln(self.end_tag(tag) + ")")
|
||||
|
||||
def generate_Math(self, e: Math):
|
||||
formats = {
|
||||
"DisplayMath": True,
|
||||
"InlineMath": False
|
||||
}
|
||||
rawhtml = self.katexClient.render(e.text, {"displayMode": formats[e.format]})
|
||||
if (e.format == "InlineMath"):
|
||||
self.write(rawhtml)
|
||||
else:
|
||||
self.writeraw(rawhtml)
|
||||
|
||||
def generate_RawInline(self, e: RawInline):
|
||||
if e.format == "html":
|
||||
self.write(e.text)
|
||||
|
||||
def generate_RawBlock(self, e: RawBlock):
|
||||
if e.format == "html":
|
||||
self.writeraw(e.text)
|
||||
|
||||
def generate_Link(self, e: Link):
|
||||
attributes = {}
|
||||
attributes["href"] = e.url
|
||||
if e.title:
|
||||
attributes["title"] = e.title
|
||||
self.generate_simple_tag(e, attributes=self.common_attributes(e) | attributes)
|
||||
|
||||
def generate_OrderedList(self, e: OrderedList):
|
||||
attributes = {}
|
||||
if e.start and e.start != 1:
|
||||
attributes["start"] = str(e.start)
|
||||
html_styles = {
|
||||
"Decimal": "1",
|
||||
"LowerRoman": "i",
|
||||
"UpperRoman:": "I",
|
||||
"LowerAlpha": "a",
|
||||
"UpperAlpha": "A"
|
||||
}
|
||||
if e.style and e.style != "DefaultStyle":
|
||||
attributes["type"] = html_styles[e.style]
|
||||
# FIXME: Delimeter styles: 1. 1) (1)
|
||||
self.generate_simple_tag(e, attributes=self.common_attributes(e) | attributes)
|
||||
|
||||
def generate_TableCell(self, e: TableCell):
|
||||
attributes = self.common_attributes(e)
|
||||
if e.colspan != 1:
|
||||
attributes["colspan"] = str(e.colspan)
|
||||
if e.rowspan != 1:
|
||||
attributes["rowspan"] = str(e.rowspan)
|
||||
aligns = {
|
||||
"AlignLeft": "left",
|
||||
"AlignRight": "right",
|
||||
"AlignCenter": "center"
|
||||
}
|
||||
if e.alignment and e.alignment != "AlignDefault":
|
||||
attributes["style"] = attributes.get("style", "")+f"text-align: {aligns[e.alignment]};"
|
||||
self.generate_simple_tag(e, attributes=attributes)
|
||||
|
||||
def generate_Cite(self, e: Cite):
|
||||
self.generate_simple_tag(e, tag="a", attributes=self.common_attributes(e) | {"href": f"#ref-{e.citations[0].id}"})
|
||||
|
||||
# These are also disabled in pandoc so they shouldn't appear in the AST at all.
|
||||
def generate_Citation(self, e: Citation):
|
||||
self.writeln("<!-- FIXME: Citations not implemented -->")
|
||||
|
||||
def generate_Definition(self, e: Definition):
|
||||
self.writeln("<!-- FIXME: Definitions not implemented -->")
|
||||
|
||||
def generate_DefinitionItem(self, e: DefinitionItem):
|
||||
self.writeln("<!-- FIXME: DefinitionItems not implemented -->")
|
||||
|
||||
def generate_DefinitionList(self, e: DefinitionList):
|
||||
self.writeln("<!-- FIXME: DefinitionLists not implemented -->")
|
||||
|
||||
|
||||
class StandaloneHTMLGenerator(HTMLGenerator):
|
||||
def generate_Doc(self, e: Doc):
|
||||
self.writeraw("<!DOCTYPE html>")
|
||||
self.writeln(self.start_tag("html", attributes={"lang": e.get_metadata("lang", None, True)}))
|
||||
self.writeln(self.start_tag("head"))
|
||||
self.indent_more()
|
||||
self.writeln(self.single_tag("meta", attributes={"charset": "utf-8"}))
|
||||
self.writeln(self.single_tag("meta", attributes={"viewport": "width=device-width, initial-scale=1.0"}))
|
||||
self.writeln(self.single_tag("link", attributes={"rel": "stylesheet", "href": "https://cdn.jsdelivr.net/npm/katex@0.16.4/dist/katex.min.css", "integrity":"sha384-vKruj+a13U8yHIkAyGgK1J3ArTLzrFGBbBc0tDp4ad/EyewESeXE/Iv67Aj8gKZ0", "crossorigin":"anonymous"}))
|
||||
if "title" in e.metadata:
|
||||
self.write(self.start_tag("title"))
|
||||
self.generate(e.metadata["title"])
|
||||
self.write(self.end_tag("title"))
|
||||
self.endln()
|
||||
if "html-head-includes" in e.metadata:
|
||||
self.generate(e.metadata["html-head-includes"])
|
||||
self.indent_less()
|
||||
self.writeln(self.end_tag("head"))
|
||||
self.writeln(self.start_tag("body"))
|
||||
self.indent_more()
|
||||
super().generate_Doc(e)
|
||||
self.indent_less()
|
||||
self.writeln(self.end_tag("body"))
|
||||
self.writeln(self.end_tag("html"))
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,260 +0,0 @@
|
|||
from typing import Union
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from PIL import Image
|
||||
|
||||
from .context import Context
|
||||
|
||||
|
||||
class FileInWrongDirError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ConversionProgramError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InkscapeError(ConversionProgramError):
|
||||
pass
|
||||
|
||||
|
||||
class ImageMagickError(ConversionProgramError):
|
||||
pass
|
||||
|
||||
|
||||
class AsyError(ConversionProgramError):
|
||||
pass
|
||||
|
||||
|
||||
class ImageProcessorNamespace:
|
||||
public_dir: str
|
||||
cache_dir: str
|
||||
lookup_dirs: list[str]
|
||||
web_path: str
|
||||
include_src: bool
|
||||
|
||||
def __init__(self, public_dir: str, web_path: str, cache_dir: str, lookup_dirs: list[str], include_src: bool):
|
||||
self.public_dir = public_dir
|
||||
self.cache_dir = cache_dir
|
||||
self.lookup_dirs = lookup_dirs
|
||||
self.web_path = web_path if web_path[-1] != "/" else web_path[:-1]
|
||||
self.include_src = include_src
|
||||
|
||||
class ImageProcessorSearcher:
|
||||
def get_lookup_dirs(self) -> list[str]:
|
||||
return []
|
||||
|
||||
def get_cache_dir(self) -> str:
|
||||
return ""
|
||||
|
||||
def get_public_dir(self) -> str:
|
||||
return ""
|
||||
|
||||
def get_web_path(self) -> str:
|
||||
return ""
|
||||
|
||||
def find_image_in_dir(self, input_filename: str, dir: str) -> Union[str, None]:
|
||||
if os.path.isfile(dir + "/" + input_filename):
|
||||
return dir + "/" + input_filename
|
||||
else:
|
||||
return None
|
||||
|
||||
def find_image(self, input_filename: str) -> Union[str, None]:
|
||||
for dir in self.get_lookup_dirs():
|
||||
image = self.find_image_in_dir(input_filename, dir)
|
||||
if image:
|
||||
return image
|
||||
return None
|
||||
|
||||
def publish_image(self, target_name, relative: bool=True) -> str:
|
||||
cache_path = self.get_cache_dir() + "/" + target_name
|
||||
if not os.path.isfile(cache_path):
|
||||
raise FileNotFoundError(f'Image {target_name} not cached')
|
||||
target_path = self.get_public_dir() + "/" + target_name
|
||||
try:
|
||||
if os.path.exists(target_path):
|
||||
if os.path.getmtime(cache_path) > os.path.getmtime(target_path):
|
||||
os.remove(target_path)
|
||||
os.link(cache_path, target_path)
|
||||
else:
|
||||
os.link(cache_path, target_path)
|
||||
except OSError as e:
|
||||
if e.errno == 18: # Invalid cross-device link: cache and public dirs are on different devices, don't hardlink, copy
|
||||
shutil.copyfile(cache_path, target_path)
|
||||
else:
|
||||
raise e
|
||||
return target_name if relative else target_path
|
||||
|
||||
|
||||
|
||||
class ImageProcessorCacheSearcher(ImageProcessorSearcher):
|
||||
cache_dir: str
|
||||
|
||||
def __init__(self, cache_dir: str):
|
||||
self.cache_dir = cache_dir
|
||||
if not os.path.exists(self.cache_dir):
|
||||
os.makedirs(self.cache_dir, exist_ok=True)
|
||||
|
||||
def get_lookup_dirs(self) -> list[str]:
|
||||
return [self.cache_dir]
|
||||
|
||||
def get_cache_dir(self) -> str:
|
||||
return self.cache_dir
|
||||
|
||||
def get_public_dir(self) -> str:
|
||||
return ""
|
||||
|
||||
def get_web_path(self) -> str:
|
||||
return ""
|
||||
|
||||
def publish_image(self, target_name, relative: bool=True) -> str:
|
||||
raise NotImplementedError();
|
||||
|
||||
class ImageProcessorNamespaceSearcher(ImageProcessorSearcher):
|
||||
namespace: ImageProcessorNamespace
|
||||
rel_dir: str
|
||||
source_dir: str
|
||||
|
||||
def __init__(self, namespace: ImageProcessorNamespace, rel_dir: str, source_dir: str):
|
||||
self.namespace = namespace
|
||||
self.rel_dir = rel_dir
|
||||
self.source_dir = source_dir
|
||||
|
||||
def get_lookup_dirs(self) -> list[str]:
|
||||
return self.namespace.lookup_dirs + ([self.source_dir] if self.namespace.include_src else [])
|
||||
|
||||
def transform_path(self, path: str) -> str:
|
||||
return path.replace("$dir", self.rel_dir)
|
||||
|
||||
def get_cache_dir(self) -> str:
|
||||
cache_dir = self.transform_path(self.namespace.cache_dir)
|
||||
if not os.path.exists(cache_dir):
|
||||
os.makedirs(cache_dir, exist_ok=True)
|
||||
return cache_dir
|
||||
|
||||
def get_public_dir(self) -> str:
|
||||
public_dir = self.transform_path(self.namespace.public_dir)
|
||||
if not os.path.exists(public_dir):
|
||||
os.makedirs(public_dir, exist_ok=True)
|
||||
return public_dir
|
||||
|
||||
def get_web_path(self) -> str:
|
||||
return self.transform_path(self.namespace.web_path)
|
||||
|
||||
def get_cache_searcher(self) -> ImageProcessorCacheSearcher:
|
||||
return ImageProcessorCacheSearcher(self.get_cache_dir())
|
||||
|
||||
class ImageProcessor:
|
||||
namespaces: dict[str, ImageProcessorNamespace]
|
||||
|
||||
def __init__(self, namespaces: dict[str, ImageProcessorNamespace]):
|
||||
self.namespaces = namespaces
|
||||
|
||||
def get_namespace_by_path(self, path: str) -> ImageProcessorNamespace:
|
||||
return self.namespaces[path.split(":")[0] if ":" in path else ""]
|
||||
|
||||
def get_path_without_namespace(self, path: str) -> str:
|
||||
if len(path.split(":")) <= 1:
|
||||
return path
|
||||
return ":".join(path.split(":")[1:])
|
||||
|
||||
def get_searcher_by_path(self, path: str, rel_dir: str, source_dir: str) -> ImageProcessorNamespaceSearcher:
|
||||
return ImageProcessorNamespaceSearcher(self.get_namespace_by_path(path), rel_dir, source_dir)
|
||||
|
||||
def process_image(self, input_filename: str, format: str, searcher: ImageProcessorSearcher, context: Context=None, width: int=None, height:int=None, quality: int=None, dpi: int=None, fit: bool=True, deps: list[str]=[]) -> str:
|
||||
name = os.path.basename(input_filename)
|
||||
base, ext = os.path.splitext(name)
|
||||
ext = ext[1:]
|
||||
full_path = searcher.find_image(input_filename)
|
||||
if full_path is None:
|
||||
raise FileNotFoundError(f'Image {input_filename} not found in {searcher.get_lookup_dirs()}.')
|
||||
|
||||
if format == "jpg":
|
||||
format = "jpeg"
|
||||
|
||||
if format == "":
|
||||
format = ext
|
||||
|
||||
# Locate all dependencies
|
||||
deps_full = [full_path]
|
||||
for dep in deps:
|
||||
dep_full_path = searcher.find_image(dep)
|
||||
if dep_full_path is None:
|
||||
raise FileNotFoundError(f'Image dependency {dep} not found.')
|
||||
deps_full.append(dep_full_path)
|
||||
|
||||
# Generate filename from arguments
|
||||
suffix = ""
|
||||
geometry = None
|
||||
if width is not None or height is not None:
|
||||
geometry = f'{width if width is not None else ""}x{height if height is not None else ""}{"" if fit else "!"}'
|
||||
suffix += "_"+geometry
|
||||
if quality is not None:
|
||||
suffix += f'_q{quality}'
|
||||
target_name = base+suffix+"."+format
|
||||
target_path = searcher.get_cache_dir() + "/" + target_name
|
||||
|
||||
# Only regenerate if the file doesn't already exist and no dependencies are newer
|
||||
if not os.path.isfile(target_path) or self.is_outdated(target_path, deps_full):
|
||||
|
||||
# If the format is the same or it is just a different extension for
|
||||
# the same format, just copy it.
|
||||
if (((ext == format)
|
||||
or (ext == "epdf" and format == "pdf")
|
||||
or (ext == "jpg" and format == "jpeg"))
|
||||
and width is None and height is None and quality is None and dpi is None):
|
||||
shutil.copyfile(full_path, target_path)
|
||||
|
||||
# Try to find the converted filename in lookup_dirs, if you find
|
||||
# it, don't convert, just copy.
|
||||
elif searcher.find_image(target_name) is not None and not self.is_outdated(searcher.find_image(target_name), deps):
|
||||
shutil.copyfile(searcher.find_image(target_name), target_path)
|
||||
|
||||
# Process asymptote
|
||||
elif ext == "asy":
|
||||
# Collect dependencies
|
||||
deps_dir = searcher.get_cache_dir() + "/" + name + "_deps"
|
||||
if not os.path.isdir(deps_dir):
|
||||
os.mkdir(deps_dir)
|
||||
for dep_full in deps_full:
|
||||
dep = os.path.basename(dep_full)
|
||||
if not os.path.isfile(deps_dir + "/" + dep) or os.path.getmtime(deps_dir + "/" + dep) < os.path.getmtime(dep_full):
|
||||
shutil.copyfile(dep_full, deps_dir + "/" + dep)
|
||||
dpi_arg = ['-render', str(dpi/72)] if dpi is not None else []
|
||||
if subprocess.run(['asy', name, '-o', target_name, '-f', format, *dpi_arg], cwd=deps_dir).returncode != 0:
|
||||
raise AsyError(f"Could not convert '{full_path}' to '{format}'")
|
||||
shutil.move(deps_dir + "/" + target_name, searcher.get_cache_dir() + "/" + target_name)
|
||||
|
||||
# Convert SVGs using inkscape
|
||||
elif ext == "svg":
|
||||
width_arg = ['--export-width', str(width)] if width is not None else []
|
||||
height_arg = ['--export-height', str(height)] if height is not None else []
|
||||
dpi_arg = ['--export-dpi', str(dpi)] if dpi is not None else []
|
||||
if subprocess.run(['inkscape', full_path, '-o', target_path, *width_arg, *height_arg, *dpi_arg]).returncode != 0:
|
||||
raise InkscapeError(f"Could not convert '{full_path}' to '{format}'")
|
||||
|
||||
# Convert everything else using ImageMagick.
|
||||
else:
|
||||
resize_arg = ['-resize', str(geometry)] if geometry is not None else []
|
||||
density_arg = ['-density', str(dpi)] if dpi is not None else []
|
||||
quality_arg = ['-quality', str(quality)] if quality is not None else []
|
||||
if subprocess.run(['convert', *density_arg, full_path, *resize_arg, *quality_arg, target_path]).returncode != 0:
|
||||
raise ImageMagickError(f"Could not convert '{full_path}' to '{format}'")
|
||||
|
||||
if context is not None:
|
||||
context.add_deps(deps_full)
|
||||
return target_name
|
||||
|
||||
def is_outdated(self, target: str, deps: list[str]):
|
||||
target_timestamp = os.path.getmtime(target)
|
||||
for dep in deps:
|
||||
dep_timestamp = os.path.getmtime(dep)
|
||||
if dep_timestamp > target_timestamp:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_image_size(self, full_path: str) -> tuple[int, int]:
|
||||
# Getting image size using ImageMagick is slow. VERY
|
||||
return Image.open(full_path).size
|
||||
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 211cb2010e23265be599819c5f79f66f0abd62d1
|
|
@ -1,106 +0,0 @@
|
|||
import socket
|
||||
import subprocess
|
||||
import tempfile
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
|
||||
class KatexError(Exception):
|
||||
pass
|
||||
|
||||
class NPMNotFoundError(Exception):
|
||||
pass
|
||||
|
||||
class KatexServerError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class KatexClient:
|
||||
_client: socket.socket
|
||||
_server_process: subprocess.Popen[bytes]
|
||||
_socket_file: str
|
||||
_temp_dir: tempfile.TemporaryDirectory[str]
|
||||
_connected: bool
|
||||
_katex_server_path: str
|
||||
|
||||
def __init__(self, socket: str=None, connect: bool=True, katex_server_path: str=None):
|
||||
self._katex_server_path = katex_server_path
|
||||
if socket is not None:
|
||||
self._socket_file = socket
|
||||
else:
|
||||
self.open_socket()
|
||||
if connect:
|
||||
self.connect()
|
||||
self._client.sendall("init\n".encode("utf-8")) # Reinitialize KaTeX Server in case it was reused.
|
||||
self._connected = True
|
||||
else:
|
||||
self._connected = False
|
||||
|
||||
def open_socket(self):
|
||||
# Create temporary directory for socket
|
||||
self._temp_dir = tempfile.TemporaryDirectory(prefix='formatitko')
|
||||
self._socket_file = self._temp_dir.name + "/katex-socket"
|
||||
|
||||
if self._katex_server_path is None:
|
||||
|
||||
srcdir = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
# Test if `node_modules` directory exists and if not, run `npm install`
|
||||
if not os.path.isdir(srcdir + "/katex-server/node_modules"):
|
||||
print("Installing node dependencies for the first time...")
|
||||
npm = shutil.which("npm") or shutil.which("yarnpkg")
|
||||
if npm is None:
|
||||
raise NPMNotFoundError("npm not found. Node.js is required to use KaTeX.")
|
||||
subprocess.run([npm, "install"], cwd=srcdir+"/katex-server", check=True)
|
||||
|
||||
self._katex_server_path = srcdir + "/katex-server/index.mjs"
|
||||
|
||||
self._server_process = subprocess.Popen(["node", self._katex_server_path, self._socket_file], stdout=subprocess.PIPE)
|
||||
|
||||
ok = self._server_process.stdout.readline()
|
||||
if ok != b"OK\n":
|
||||
raise KatexServerError("Failed to connect to katex-server")
|
||||
|
||||
def connect(self):
|
||||
self._client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
self._client.connect(self._socket_file)
|
||||
|
||||
def get_socket(self):
|
||||
return self._socket_file
|
||||
|
||||
def render(self, tex: str, options: dict={}):
|
||||
if not self._connected:
|
||||
raise KatexServerError("KatexClient not connected to Katex server. It should be initialized with connect=True.")
|
||||
# Send formulas to translate
|
||||
self._client.sendall((json.dumps({"formulas":[{"tex":tex}], "options":options})+"\n").encode("utf-8"))
|
||||
|
||||
# Receive response
|
||||
data = self._client.recv(4096)
|
||||
while data[-1] != 0x0a:
|
||||
data += self._client.recv(128)
|
||||
response = json.loads(data)
|
||||
|
||||
if "error" in response:
|
||||
raise KatexServerError(response["error"])
|
||||
if "error" in response["results"][0]:
|
||||
raise KatexError(response["results"][0]["error"] + " in $" + tex + "$")
|
||||
else:
|
||||
return response["results"][0]["html"]
|
||||
|
||||
# Special commands implemented in the JS file for grouping defs together.
|
||||
def begingroup(self):
|
||||
if not self._connected:
|
||||
raise KatexServerError("KatexClient not connected to Katex server. It should be initialized with connect=True.")
|
||||
self._client.sendall("begingroup\n".encode("utf-8"))
|
||||
|
||||
def endgroup(self):
|
||||
if not self._connected:
|
||||
raise KatexServerError("KatexClient not connected to Katex server. It should be initialized with connect=True.")
|
||||
self._client.sendall("endgroup\n".encode("utf-8"))
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, type, value, tb):
|
||||
if hasattr(self, "_server_process") and self._server_process is not None:
|
||||
self._server_process.terminate()
|
|
@ -1,104 +0,0 @@
|
|||
from panflute import Element, ListContainer, Inline, Block
|
||||
from panflute import Cite, Code, Emph, Image, LineBreak, Link, Math, Note, Quoted, RawInline, SmallCaps, SoftBreak, Space, Span, Str, Strikeout, Strong, Subscript, Superscript, Underline
|
||||
from panflute import BlockQuote, BulletList, Citation, CodeBlock, Definition, DefinitionItem, DefinitionList, Div, Figure, Header, HorizontalRule, LineBlock, LineItem, ListItem, MetaBlocks, MetaBool, MetaInlines, MetaList, MetaMap, MetaString, Null, OrderedList, Para, Plain, RawBlock, Table, TableBody, TableFoot, TableHead
|
||||
from panflute import TableRow, TableCell, Caption, Doc
|
||||
from typing import Union
|
||||
|
||||
from .whitespace import NBSP
|
||||
from .elements import FQuoted
|
||||
from .context import Group
|
||||
from .output_generator import OutputGenerator
|
||||
from .images import ImageProcessor
|
||||
|
||||
class LaTeXGenerator(OutputGenerator):
|
||||
imageProcessor: ImageProcessor
|
||||
|
||||
def __init__(self, output_file, imageProcessor: ImageProcessor, *args, **kwargs):
|
||||
self.imageProcessor = imageProcessor
|
||||
super().__init__(output_file, *args, **kwargs)
|
||||
|
||||
def generate(self, e: Union[Element, ListContainer]):
|
||||
if hasattr(e, "attributes") and "only" in e.attributes and e.attributes["only"] != "tex":
|
||||
return
|
||||
super().generate(e)
|
||||
|
||||
def escape_special_chars(self, text: str) -> str:
|
||||
text = text.replace("&", "\\&")
|
||||
text = text.replace("%", "\\%")
|
||||
text = text.replace("$", "\\$")
|
||||
text = text.replace("#", "\\#")
|
||||
text = text.replace("_", "\\_")
|
||||
text = text.replace("{", "\\{")
|
||||
text = text.replace("}", "\\}")
|
||||
text = text.replace("~", "\\textasciitilde{}")
|
||||
text = text.replace("^", "\\textasciicircum{}")
|
||||
text = text.replace("\\", "\\textbackslash{}")
|
||||
text = text.replace(" ", "~") # We use unicode no-break spaces to force nbsp in output
|
||||
return text
|
||||
|
||||
def start_tag(self, tag: str, attributes: dict[str,str]={}) -> str:
|
||||
return "\\" + tag + "{"
|
||||
|
||||
def end_tag(self, tag: str, attributes: dict[str,str]={}) -> str:
|
||||
return "}"
|
||||
|
||||
def single_tag(self, tag: str, attributes: dict[str,str]={}) -> str:
|
||||
return "\\" + tag + "{}"
|
||||
|
||||
|
||||
|
||||
def generate_NBSP(self, e: NBSP):
|
||||
self.write("~")
|
||||
|
||||
def generate_Null(self, e: Null):
|
||||
pass
|
||||
|
||||
def generate_LineBreak(self, e: LineBreak):
|
||||
self.write("\\\\")
|
||||
self.endln()
|
||||
|
||||
def generate_Para(self, e: Para):
|
||||
self.generate(e.content)
|
||||
self.writeln("") # This ensures an empty line
|
||||
|
||||
def generate_Plain(self, e: Plain):
|
||||
self.generate(e.content)
|
||||
|
||||
def generate_Span(self, e: Plain):
|
||||
self.generate(e.content)
|
||||
|
||||
def generate_Header(self, e: Header):
|
||||
tag = {
|
||||
1: "section",
|
||||
2: "subsection",
|
||||
3: "subsubsection",
|
||||
4: "paragraph",
|
||||
5: "subparagraph",
|
||||
6: "textbf"
|
||||
}
|
||||
|
||||
self.generate_simple_tag(e, tag=tag[e.level])
|
||||
|
||||
def generate_HorizontalRule(self, e: HorizontalRule):
|
||||
self.writeln("\\begin{center}\\rule{0.5\\linewidth}{0.5pt}\\end{center}")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# These are also disabled in pandoc so they shouldn't appear in the AST at all.
|
||||
def generate_Citation(self, e: Citation):
|
||||
self.writeln("% FIXME: Citations not implemented")
|
||||
|
||||
def generate_Cite(self, e: Cite):
|
||||
self.writeln("% FIXME: Cites not implemented")
|
||||
|
||||
def generate_Definition(self, e: Definition):
|
||||
self.writeln("% FIXME: Definitions not implemented")
|
||||
|
||||
def generate_DefinitionItem(self, e: DefinitionItem):
|
||||
self.writeln("% FIXME: DefinitionItems not implemented")
|
||||
|
||||
def generate_DefinitionList(self, e: DefinitionList):
|
||||
self.writeln("% FIXME: DefinitionLists not implemented")
|
||||
|
|
@ -1,391 +0,0 @@
|
|||
from panflute import Element, ListContainer, Inline, Block
|
||||
from panflute import Cite, Code, Emph, Image, LineBreak, Link, Math, Note, Quoted, RawInline, SmallCaps, SoftBreak, Space, Span, Str, Strikeout, Strong, Subscript, Superscript, Underline
|
||||
from panflute import BlockQuote, BulletList, Citation, CodeBlock, Definition, DefinitionItem, DefinitionList, Div, Figure, Header, HorizontalRule, LineBlock, LineItem, ListItem, MetaBlocks, MetaBool, MetaInlines, MetaList, MetaMap, MetaString, Null, OrderedList, Para, Plain, RawBlock, Table, TableBody, TableFoot, TableHead
|
||||
from panflute import TableRow, TableCell, Caption, Doc
|
||||
from panflute import MetaValue
|
||||
from typing import Union, Callable
|
||||
|
||||
from .whitespace import NBSP
|
||||
from .elements import FQuoted, FileLink
|
||||
from .context import Group, InlineGroup, BlockGroup, Context
|
||||
from .whitespace import Whitespace
|
||||
from .command import BlockCommand, InlineCommand, CodeCommand, Command
|
||||
from .output_generator import FormatitkoRecursiveError
|
||||
|
||||
ELCl = Union[Element, ListContainer, list[Union[Element, ListContainer]]]
|
||||
|
||||
class DoubleDocError(Exception):
|
||||
"TransformProcessor should only ever see a single Doc."
|
||||
pass
|
||||
|
||||
class NOPProcessor:
|
||||
TYPE_DICT: dict[type, Callable]
|
||||
context: Union[Context, None] = None
|
||||
|
||||
class UnknownElementError(Exception):
|
||||
f"An unknown Element has been passed to the NOPProcessor, probably because panflute introduced a new one."
|
||||
pass
|
||||
|
||||
def __init__(self):
|
||||
self.TYPE_DICT = {
|
||||
TableRow: self.transform_TableRow,
|
||||
TableCell: self.transform_TableCell,
|
||||
Caption: self.transform_Caption,
|
||||
Doc: self.transform_Doc,
|
||||
LineItem: self.transform_LineItem,
|
||||
ListItem: self.transform_ListItem,
|
||||
|
||||
BlockQuote: self.transform_BlockQuote,
|
||||
BulletList: self.transform_BulletList,
|
||||
Citation: self.transform_Citation,
|
||||
CodeBlock: self.transform_CodeBlock,
|
||||
Definition: self.transform_Definition,
|
||||
DefinitionItem: self.transform_DefinitionItem,
|
||||
DefinitionList: self.transform_DefinitionList,
|
||||
Div: self.transform_Div,
|
||||
Figure: self.transform_Figure,
|
||||
Header: self.transform_Header,
|
||||
HorizontalRule: self.transform_HorizontalRule,
|
||||
LineBlock: self.transform_LineBlock,
|
||||
MetaBlocks: self.transform_MetaBlocks,
|
||||
MetaBool: self.transform_MetaBool,
|
||||
MetaInlines: self.transform_MetaInlines,
|
||||
MetaList: self.transform_MetaList,
|
||||
MetaMap: self.transform_MetaMap,
|
||||
MetaString: self.transform_MetaString,
|
||||
Null: self.transform_Null,
|
||||
OrderedList: self.transform_OrderedList,
|
||||
Para: self.transform_Para,
|
||||
Plain: self.transform_Plain,
|
||||
RawBlock: self.transform_RawBlock,
|
||||
Table: self.transform_Table,
|
||||
TableBody: self.transform_TableBody,
|
||||
TableFoot: self.transform_TableFoot,
|
||||
TableHead: self.transform_TableHead,
|
||||
Group: self.transform_Group,
|
||||
InlineGroup: self.transform_InlineGroup,
|
||||
BlockGroup: self.transform_BlockGroup,
|
||||
|
||||
Cite: self.transform_Cite,
|
||||
Code: self.transform_Code,
|
||||
Emph: self.transform_Emph,
|
||||
Image: self.transform_Image,
|
||||
LineBreak: self.transform_LineBreak,
|
||||
Link: self.transform_Link,
|
||||
Math: self.transform_Math,
|
||||
Note: self.transform_Note,
|
||||
Quoted: self.transform_Quoted,
|
||||
RawInline: self.transform_RawInline,
|
||||
SmallCaps: self.transform_SmallCaps,
|
||||
SoftBreak: self.transform_SoftBreak,
|
||||
Space: self.transform_Space,
|
||||
Span: self.transform_Span,
|
||||
Str: self.transform_Str,
|
||||
Strikeout: self.transform_Strikeout,
|
||||
Strong: self.transform_Strong,
|
||||
Subscript: self.transform_Subscript,
|
||||
Superscript: self.transform_Superscript,
|
||||
Underline: self.transform_Underline,
|
||||
NBSP: self.transform_NBSP,
|
||||
FQuoted: self.transform_FQuoted,
|
||||
FileLink: self.transform_FileLink,
|
||||
|
||||
InlineCommand: self.transform_InlineCommand,
|
||||
BlockCommand: self.transform_BlockCommand,
|
||||
CodeCommand: self.transform_CodeCommand
|
||||
}
|
||||
|
||||
def get_pretransformers(self) -> list[Callable[[ELCl],ELCl]]:
|
||||
return []
|
||||
|
||||
def get_posttransformers(self) -> list[Callable[[ELCl],ELCl]]:
|
||||
return []
|
||||
|
||||
def transform(self, e: ELCl) -> ELCl:
|
||||
try:
|
||||
if isinstance(e, list):
|
||||
return self.transform_list(e)
|
||||
elif isinstance(e, ListContainer):
|
||||
return self.transform_ListContainer(e)
|
||||
|
||||
for transformer in self.get_pretransformers():
|
||||
e = transformer(e)
|
||||
|
||||
try:
|
||||
method = self.TYPE_DICT[type(e)]
|
||||
except KeyError:
|
||||
raise self.UnknownElementError(type(e))
|
||||
|
||||
e = method(e)
|
||||
|
||||
for transformer in self.get_posttransformers():
|
||||
e = transformer(e)
|
||||
|
||||
return e
|
||||
except FormatitkoRecursiveError as err:
|
||||
if not isinstance(e, ListContainer):
|
||||
err.add_element(e)
|
||||
raise err
|
||||
except Exception as err:
|
||||
raise FormatitkoRecursiveError(e, self.context) from err
|
||||
|
||||
def transform_list(self, e: list[Union[Element, ListContainer]]) -> list[Union[Element, ListContainer]]:
|
||||
i = 0
|
||||
while i < len(e): # The length of the list can change mid-transformation, so we need to check the length each time
|
||||
e[i] = self.transform(e[i])
|
||||
i-=-1
|
||||
return e
|
||||
|
||||
def transform_ListContainer(self, e: ListContainer) -> ListContainer:
|
||||
i = 0
|
||||
while i < len(e): # The length of the list can change mid-transformation, so we need to check the length each time
|
||||
e[i] = self.transform(e[i])
|
||||
i-=-1
|
||||
return e
|
||||
|
||||
|
||||
def transform_TableRow(self, e: TableRow) -> TableRow:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_TableCell(self, e: TableCell) -> TableCell:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_Caption(self, e: Caption) -> Caption:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_LineItem(self, e: LineItem) -> LineItem:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_ListItem(self, e: ListItem) -> ListItem:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_BlockQuote(self, e: BlockQuote) -> BlockQuote:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_BulletList(self, e: BulletList) -> BulletList:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_Citation(self, e: Citation) -> Citation:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_Definition(self, e: Definition) -> Definition:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_DefinitionItem(self, e: DefinitionItem) -> DefinitionItem:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_DefinitionList(self, e: DefinitionList) -> DefinitionList:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_Header(self, e: Header) -> Header:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_LineBlock(self, e: LineBlock) -> LineBlock:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_MetaBlocks(self, e: MetaBlocks) -> MetaBlocks:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_MetaBool(self, e: MetaBool) -> MetaBool:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_MetaInlines(self, e: MetaInlines) -> MetaInlines:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_MetaList(self, e: MetaList) -> MetaList:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_MetaMap(self, e: MetaMap) -> MetaMap:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_MetaString(self, e: MetaString) -> MetaString:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_OrderedList(self, e: OrderedList) -> OrderedList:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_Para(self, e: Para) -> Para:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_Plain(self, e: Plain) -> Plain:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_TableBody(self, e: TableBody) -> TableBody:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_TableFoot(self, e: TableFoot) -> TableFoot:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_TableHead(self, e: TableHead) -> TableHead:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_Group(self, e: Group) -> Group:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_InlineGroup(self, e: InlineGroup) -> InlineGroup:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_BlockGroup(self, e: BlockGroup) -> BlockGroup:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_Cite(self, e: Cite) -> Cite:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_Emph(self, e: Emph) -> Emph:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_Link(self, e: Link) -> Link:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_Note(self, e: Note) -> Note:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_SmallCaps(self, e: SmallCaps) -> SmallCaps:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_Strikeout(self, e: Strikeout) -> Strikeout:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_Strong(self, e: Strong) -> Strong:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_Subscript(self, e: Subscript) -> Subscript:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_Superscript(self, e: Superscript) -> Superscript:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_Underline(self, e: Underline) -> Underline:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_FQuoted(self, e: FQuoted) -> FQuoted:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_FileLink(self, e: FileLink) -> FileLink:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_Figure(self, e: Figure) -> Figure:
|
||||
e.content = self.transform(e.content)
|
||||
e.caption = self.transform(e.caption)
|
||||
return e
|
||||
|
||||
def transform_Table(self, e: Table) -> Table:
|
||||
e.head = self.transform(e.head)
|
||||
e.content = self.transform(e.content)
|
||||
e.foot = self.transform(e.foot)
|
||||
return e
|
||||
|
||||
def transform_Doc(self, e: Doc) -> Doc:
|
||||
if self.context is not None:
|
||||
raise DoubleDocError()
|
||||
self.context = Context(e, self.root_file_path)
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_Quoted(self, e: Quoted) -> Quoted:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_Image(self, e: Image) -> Image:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_Div(self, e: Div) -> Div:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_Span(self, e: Span) -> Span:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_CodeBlock(self, e: CodeBlock) -> CodeBlock:
|
||||
return e
|
||||
|
||||
def transform_Command(self, e: Command) -> Command:
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
def transform_InlineCommand(self, e: InlineCommand) -> Span:
|
||||
return self.transform_Command(e)
|
||||
|
||||
def transform_BlockCommand(self, e: BlockCommand) -> Div:
|
||||
return self.transform_Command(e)
|
||||
|
||||
def transform_CodeCommand(self, e: CodeCommand) -> Div:
|
||||
return self.transform_Command(e)
|
||||
|
||||
def transform_Whitespace(self, e: Whitespace) -> Whitespace:
|
||||
return e
|
||||
|
||||
def transform_SoftBreak(self, e: SoftBreak) -> Whitespace:
|
||||
return self.transform_Whitespace(e)
|
||||
|
||||
def transform_Space(self, e: Space) -> Whitespace:
|
||||
return self.transform_Whitespace(e)
|
||||
|
||||
def transform_NBSP(self, e: NBSP) -> NBSP:
|
||||
return e
|
||||
|
||||
def transform_Str(self, e: Str) -> Str:
|
||||
return e
|
||||
|
||||
def transform_RawInline(self, e: RawInline) -> RawInline:
|
||||
return e
|
||||
|
||||
def transform_Math(self, e: Math) -> Math:
|
||||
return e
|
||||
|
||||
def transform_LineBreak(self, e: LineBreak) -> LineBreak:
|
||||
return e
|
||||
|
||||
def transform_Code(self, e: Code) -> Code:
|
||||
return e
|
||||
|
||||
def transform_RawBlock(self, e: RawBlock) -> RawBlock:
|
||||
return e
|
||||
|
||||
def transform_Null(self, e: Null) -> Null:
|
||||
return e
|
||||
|
||||
def transform_HorizontalRule(self, e: HorizontalRule) -> HorizontalRule:
|
||||
return e
|
|
@ -1,550 +0,0 @@
|
|||
from panflute import Element, ListContainer, Inline, Block
|
||||
from panflute import Cite, Code, Emph, Image, LineBreak, Link, Math, Note, Quoted, RawInline, SmallCaps, SoftBreak, Space, Span, Str, Strikeout, Strong, Subscript, Superscript, Underline
|
||||
from panflute import BlockQuote, BulletList, Citation, CodeBlock, Definition, DefinitionItem, DefinitionList, Div, Figure, Header, HorizontalRule, LineBlock, LineItem, ListItem, MetaBlocks, MetaBool, MetaInlines, MetaList, MetaMap, MetaString, Null, OrderedList, Para, Plain, RawBlock, Table, TableBody, TableFoot, TableHead
|
||||
from panflute import TableRow, TableCell, Caption, Doc
|
||||
from panflute import MetaValue
|
||||
from panflute import stringify
|
||||
from typing import Union, Callable
|
||||
|
||||
from .whitespace import NBSP
|
||||
from .elements import FQuoted, FileLink
|
||||
from .context import Group, InlineGroup, BlockGroup, Context
|
||||
|
||||
|
||||
import sys
|
||||
|
||||
class UnknownElementError(Exception):
|
||||
"An unknown Element has been passed to the OutputGenerator, probably because panflute introduced a new one."
|
||||
pass
|
||||
|
||||
|
||||
class FormatitkoRecursiveError(Exception):
|
||||
"A generic exception which wraps other exceptions and adds element-based traceback"
|
||||
elements: list[Union[Element, ListContainer, list[Union[Element, ListContainer]]]]
|
||||
context: Context
|
||||
|
||||
def __init__(self, e: Union[Element, ListContainer, list[Union[Element, ListContainer]]], context: Context, *args):
|
||||
self.elements = [e]
|
||||
self.context = context
|
||||
super().__init__(args)
|
||||
|
||||
def add_element(self, e: Union[Element, ListContainer, list[Union[Element, ListContainer]]]):
|
||||
self.elements.append(e)
|
||||
|
||||
def pretty_print(self, tracebacklimit: int=0):
|
||||
def eprint(*args, **kwargs):
|
||||
print(*args, file=sys.stderr, **kwargs)
|
||||
|
||||
def print_filename_recursive(context: Context):
|
||||
return context.path +\
|
||||
((" (included from " + print_filename_recursive(context.parent) + ")") if context.parent else "")
|
||||
eprint(f"Error occured in file {print_filename_recursive(self.context)} in ", end="")
|
||||
line = None
|
||||
for i in range(len(self.elements)-1, 0, -1):
|
||||
if hasattr(self.elements[i], "content") and len(self.elements[i].content) > 0 and isinstance(self.elements[i].content[0], Inline) and line is None:
|
||||
line = self.elements[i]
|
||||
eprint(type(self.elements[i]).__name__ + "[" + (str(self.elements[i-1].index) if isinstance(self.elements[i-1].index, int) else "") + "]", end=": ")
|
||||
if line:
|
||||
eprint()
|
||||
eprint('on line: "' + stringify(line).strip() + '"', end="")
|
||||
eprint()
|
||||
eprint("in element: " + str(self.elements[0]).replace("\n", "\\n"))
|
||||
sys.tracebacklimit = tracebacklimit
|
||||
raise self.__cause__ from None
|
||||
|
||||
|
||||
class OutputGenerator:
|
||||
_empty_lines: int
|
||||
context: Union[Context, None]
|
||||
indent_level: int
|
||||
indent_str: str
|
||||
output_file: ...
|
||||
TYPE_DICT_BLOCK: dict[type, Callable]
|
||||
TYPE_DICT_INLINE: dict[type, Callable]
|
||||
TYPE_DICT_MISC: dict[type, Callable]
|
||||
|
||||
def __init__(self, output_file, indent_str: str="\t", initial_indent_level: int=0):
|
||||
self.output_file = output_file
|
||||
self.indent_str = indent_str
|
||||
self.indent_level = initial_indent_level
|
||||
self._empty_lines = 1
|
||||
self.context = None
|
||||
|
||||
self.TYPE_DICT_MISC = {
|
||||
TableRow: self.generate_TableRow,
|
||||
TableCell: self.generate_TableCell,
|
||||
Caption: self.generate_Caption,
|
||||
Doc: self.generate_Doc,
|
||||
LineItem: self.generate_LineItem,
|
||||
ListItem: self.generate_ListItem
|
||||
}
|
||||
|
||||
self.TYPE_DICT_BLOCK = {
|
||||
BlockQuote: self.generate_BlockQuote,
|
||||
BulletList: self.generate_BulletList,
|
||||
Citation: self.generate_Citation,
|
||||
CodeBlock: self.generate_CodeBlock,
|
||||
Definition: self.generate_Definition,
|
||||
DefinitionItem: self.generate_DefinitionItem,
|
||||
DefinitionList: self.generate_DefinitionList,
|
||||
Div: self.generate_Div,
|
||||
Figure: self.generate_Figure,
|
||||
Header: self.generate_Header,
|
||||
HorizontalRule: self.generate_HorizontalRule,
|
||||
LineBlock: self.generate_LineBlock,
|
||||
Null: self.generate_Null,
|
||||
OrderedList: self.generate_OrderedList,
|
||||
Para: self.generate_Para,
|
||||
Plain: self.generate_Plain,
|
||||
RawBlock: self.generate_RawBlock,
|
||||
Table: self.generate_Table,
|
||||
TableBody: self.generate_TableBody,
|
||||
TableFoot: self.generate_TableFoot,
|
||||
TableHead: self.generate_TableHead,
|
||||
BlockGroup: self.generate_BlockGroup
|
||||
}
|
||||
|
||||
self.TYPE_DICT_INLINE = {
|
||||
Cite: self.generate_Cite,
|
||||
Code: self.generate_Code,
|
||||
Emph: self.generate_Emph,
|
||||
Image: self.generate_Image,
|
||||
LineBreak: self.generate_LineBreak,
|
||||
Link: self.generate_Link,
|
||||
Math: self.generate_Math,
|
||||
Note: self.generate_Note,
|
||||
Quoted: self.generate_Quoted,
|
||||
RawInline: self.generate_RawInline,
|
||||
SmallCaps: self.generate_SmallCaps,
|
||||
SoftBreak: self.generate_SoftBreak,
|
||||
Space: self.generate_Space,
|
||||
Span: self.generate_Span,
|
||||
Str: self.generate_Str,
|
||||
Strikeout: self.generate_Strikeout,
|
||||
Strong: self.generate_Strong,
|
||||
Subscript: self.generate_Subscript,
|
||||
Superscript: self.generate_Superscript,
|
||||
Underline: self.generate_Underline,
|
||||
NBSP: self.generate_NBSP,
|
||||
FQuoted: self.generate_FQuoted,
|
||||
FileLink: self.generate_FileLink,
|
||||
InlineGroup: self.generate_InlineGroup
|
||||
}
|
||||
|
||||
self.TYPE_DICT_META = {
|
||||
MetaBlocks: self.generate_MetaBlocks,
|
||||
MetaBool: self.generate_MetaBool,
|
||||
MetaInlines: self.generate_MetaInlines,
|
||||
MetaMap: self.generate_MetaMap,
|
||||
MetaString: self.generate_MetaString,
|
||||
}
|
||||
|
||||
def generate(self, e: Union[Element, ListContainer, list[Union[Element, ListContainer]]]):
|
||||
try:
|
||||
if isinstance(e, Group):
|
||||
old_context = self.context
|
||||
self.context = e.context
|
||||
if isinstance(e, list):
|
||||
self.generate_list(e)
|
||||
elif isinstance(e, ListContainer):
|
||||
self.generate_ListContainer(e)
|
||||
elif isinstance(e, Inline):
|
||||
self.generate_Inline(e)
|
||||
elif isinstance(e, Block):
|
||||
self.generate_Block(e)
|
||||
elif isinstance(e, MetaValue):
|
||||
self.generate_MetaValue(e)
|
||||
elif isinstance(e, MetaList):
|
||||
self.generate_MetaList(e)
|
||||
else:
|
||||
try:
|
||||
method = self.TYPE_DICT_MISC[type(e)]
|
||||
except KeyError as err:
|
||||
raise UnknownElementError(type(e)) from err
|
||||
method(e)
|
||||
if isinstance(e, Group):
|
||||
self.context = old_context
|
||||
except FormatitkoRecursiveError as err:
|
||||
if not isinstance(e, ListContainer):
|
||||
err.add_element(e)
|
||||
raise err
|
||||
except Exception as err:
|
||||
raise FormatitkoRecursiveError(e, self.context) from err
|
||||
|
||||
def escape_special_chars(self, text: str) -> str:
|
||||
return text
|
||||
|
||||
def indent(self) -> str:
|
||||
return self.indent_str*self.indent_level
|
||||
|
||||
def indent_more(self):
|
||||
self.indent_level += 1
|
||||
|
||||
def indent_less(self):
|
||||
self.indent_level -= 1
|
||||
|
||||
def write(self, text: str=""):
|
||||
if self._empty_lines > 0:
|
||||
self.output_file.write(self.indent())
|
||||
self.output_file.write(text)
|
||||
self._empty_lines = 0
|
||||
|
||||
def writeln(self, text: str=""):
|
||||
if self._empty_lines == 0:
|
||||
self.output_file.write("\n")
|
||||
self.output_file.write(self.indent())
|
||||
self.output_file.write(text+"\n")
|
||||
self._empty_lines = 1
|
||||
|
||||
def writeraw(self, text: str=""):
|
||||
if self._empty_lines == 0:
|
||||
self.output_file.write("\n")
|
||||
self.output_file.write(text+"\n")
|
||||
self._empty_lines = 1
|
||||
|
||||
def ensure_empty(self, n: int=1):
|
||||
while self._empty_lines < n:
|
||||
self.output_file.write("\n")
|
||||
self._empty_lines+=1
|
||||
|
||||
def endln(self):
|
||||
self.ensure_empty(1)
|
||||
|
||||
def start_tag(self, tag: str, attributes: dict[str,str]={}) -> str:
|
||||
return tag
|
||||
|
||||
def end_tag(self, tag: str, attributes: dict[str,str]={}) -> str:
|
||||
return "/" + tag
|
||||
|
||||
def single_tag(self, tag: str, attributes: dict[str,str]={}) -> str:
|
||||
return "/" + tag + "/"
|
||||
|
||||
def tagname(self, e) -> str:
|
||||
return type(e).__name__
|
||||
|
||||
def common_attributes(self, e: Element) -> dict[str,str]:
|
||||
return {}
|
||||
|
||||
def generate_simple_tag(self, e: Union[Element, None]=None, tag: str="", attributes: Union[dict[str,str],None]=None, content: Union[ListContainer, Element, list[Union[Element, ListContainer]], str, None]=None, inline: Union[bool, None]=None):
|
||||
if not tag and e:
|
||||
tag = self.tagname(e)
|
||||
if attributes is None:
|
||||
if e:
|
||||
attributes = self.common_attributes(e)
|
||||
else:
|
||||
attributes = {}
|
||||
if content is None and e and hasattr(e, "content"):
|
||||
content = e.content
|
||||
if content is None and e and hasattr(e, "text"):
|
||||
content = e.text
|
||||
if inline is None and e:
|
||||
inline = isinstance(e, Inline)
|
||||
|
||||
if content is None:
|
||||
self.generate_empty_block_tag(tag, attributes)
|
||||
return
|
||||
|
||||
if inline:
|
||||
if isinstance(content, str):
|
||||
self.generate_raw_inline_tag(tag, content, attributes)
|
||||
else:
|
||||
self.generate_simple_inline_tag(tag, content, attributes)
|
||||
else:
|
||||
if isinstance(content, str):
|
||||
self.generate_raw_block_tag(tag, content, attributes)
|
||||
else:
|
||||
self.generate_simple_block_tag(tag, content, attributes)
|
||||
|
||||
def generate_simple_inline_tag(self, tag: str, content: Union[ListContainer, Element, list[Union[Element, ListContainer]]], attributes: dict[str,str]={}):
|
||||
self.write(self.start_tag(tag, attributes))
|
||||
self.generate(content)
|
||||
self.write(self.end_tag(tag))
|
||||
|
||||
def generate_simple_block_tag(self, tag: str, content: Union[ListContainer, Element, list[Union[Element, ListContainer]]], attributes: dict[str,str]={}):
|
||||
self.writeln(self.start_tag(tag, attributes))
|
||||
self.indent_more()
|
||||
self.generate(content)
|
||||
self.indent_less()
|
||||
self.writeln(self.end_tag(tag))
|
||||
|
||||
def generate_raw_inline_tag(self, tag: str, text: str, attributes: dict[str,str]={}):
|
||||
self.write(self.start_tag(tag, attributes))
|
||||
self.write(text)
|
||||
self.write(self.end_tag(tag))
|
||||
|
||||
def generate_raw_block_tag(self, tag: str, text: str, attributes: dict[str,str]={}):
|
||||
self.writeraw(self.start_tag(tag, attributes))
|
||||
self.writeraw(text)
|
||||
self.writeraw(self.end_tag(tag))
|
||||
|
||||
def generate_empty_block_tag(self, tag: str, attributes: dict[str,str]={}):
|
||||
self.writeln(self.single_tag(tag, attributes))
|
||||
|
||||
def generate_ListContainer(self, e: ListContainer):
|
||||
for child in e:
|
||||
self.generate(child)
|
||||
|
||||
def generate_list(self, e: list):
|
||||
for el in e:
|
||||
self.generate(el)
|
||||
|
||||
def generate_MetaList(self, e: MetaList):
|
||||
for child in e:
|
||||
self.generate(child)
|
||||
|
||||
def generate_MetaValue(self, e: MetaValue):
|
||||
try:
|
||||
method = self.TYPE_DICT_META[type(e)]
|
||||
except KeyError:
|
||||
self.generate(e.content)
|
||||
method(e)
|
||||
|
||||
def generate_MetaBlocks(self, e: MetaBlocks):
|
||||
self.generate(e.content)
|
||||
|
||||
def generate_MetaInlines(self, e: MetaInlines):
|
||||
self.generate(e.content)
|
||||
|
||||
def generate_MetaBool(self, e: MetaBool):
|
||||
if e.boolean:
|
||||
self.write("True")
|
||||
else:
|
||||
self.write("False")
|
||||
|
||||
def generate_MetaMap(self, e: MetaMap):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_MetaString(self, e: MetaString):
|
||||
self.write(e.text)
|
||||
|
||||
def generate_Inline(self, e: Inline):
|
||||
try:
|
||||
method = self.TYPE_DICT_INLINE[type(e)]
|
||||
except KeyError as err:
|
||||
raise UnknownElementError(type(e)) from err
|
||||
method(e)
|
||||
|
||||
def generate_Str(self, e: Str):
|
||||
self.write(self.escape_special_chars(e.text))
|
||||
|
||||
def generate_Space(self, e: Space):
|
||||
self.write(" ")
|
||||
|
||||
def generate_NBSP(self, e: NBSP):
|
||||
self.write("~")
|
||||
|
||||
def generate_SoftBreak(self, e: SoftBreak):
|
||||
self.endln()
|
||||
|
||||
def generate_FQuoted(self, e: FQuoted):
|
||||
if e.style == "cs":
|
||||
if e.quote_type == "SingleQuote":
|
||||
self.write("‚")
|
||||
self.generate(e.content)
|
||||
self.write("‘")
|
||||
elif e.quote_type == "DoubleQuote":
|
||||
self.write("„")
|
||||
self.generate(e.content)
|
||||
self.write("“")
|
||||
elif e.style == "en":
|
||||
if e.quote_type == "SingleQuote":
|
||||
self.write("‘")
|
||||
self.generate(e.content)
|
||||
self.write("’")
|
||||
elif e.quote_type == "DoubleQuote":
|
||||
self.write("“")
|
||||
self.generate(e.content)
|
||||
self.write("”")
|
||||
else:
|
||||
if e.quote_type == "SingleQuote":
|
||||
self.write("'")
|
||||
self.generate(e.content)
|
||||
self.write("'")
|
||||
elif e.quote_type == "DoubleQuote":
|
||||
self.write("\"")
|
||||
self.generate(e.content)
|
||||
self.write("\"")
|
||||
else:
|
||||
self.write("\"")
|
||||
self.generate(e.content)
|
||||
self.write("\"")
|
||||
|
||||
def generate_FileLink(self, e: FileLink):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
# Inline Elements
|
||||
def generate_Cite(self, e: Cite):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_Emph(self, e: Emph):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_Image(self, e: Image):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_Link(self, e: Link):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_Note(self, e: Note):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_Quoted(self, e: Quoted):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_SmallCaps(self, e: SmallCaps):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_Span(self, e: Span):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_Strikeout(self, e: Strikeout):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_Strong(self, e: Strong):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_Subscript(self, e: Subscript):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_Superscript(self, e: Superscript):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_Underline(self, e: Underline):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
|
||||
# Raw Inline elements
|
||||
def generate_Math(self, e: Math):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_Code(self, e: Code):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_RawInline(self, e: RawInline):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
|
||||
def generate_Block(self, e: Block):
|
||||
try:
|
||||
method = self.TYPE_DICT_BLOCK[type(e)]
|
||||
except KeyError as err:
|
||||
raise UnknownElementError(type(e)) from err
|
||||
method(e)
|
||||
|
||||
|
||||
# Block elements
|
||||
def generate_BlockQuote(self, e: BlockQuote):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_BulletList(self, e: BulletList):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_Citation(self, e: Citation):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_Definition(self, e: Definition):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_DefinitionItem(self, e: DefinitionItem):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_DefinitionList(self, e: DefinitionList):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_Div(self, e: Div):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_Header(self, e: Header):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_LineBlock(self, e: LineBlock):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_LineItem(self, e: LineItem):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_ListItem(self, e: ListItem):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_OrderedList(self, e: OrderedList):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_Para(self, e: Para):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_Plain(self, e: Plain):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_Caption(self, e: Caption):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_TableBody(self, e: TableBody):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_TableCell(self, e: TableCell):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_TableFoot(self, e: TableFoot):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_TableHead(self, e: TableHead):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_TableRow(self, e: TableRow):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_Doc(self, e: Doc):
|
||||
if "header-includes" in e.metadata: # This is the pandoc way of doing things
|
||||
self.generate(e.metadata["header-includes"])
|
||||
if "header_content" in e.metadata:
|
||||
self.generate(e.metadata["header_content"])
|
||||
self.generate_simple_tag(e)
|
||||
if "footer_content" in e.metadata:
|
||||
self.generate(e.metadata["footer_content"])
|
||||
|
||||
def generate_BlockGroup(self, e: BlockGroup):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_InlineGroup(self, e: InlineGroup):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
# Special elements with more contents
|
||||
def generate_Table(self, e: Table):
|
||||
self.generate_simple_tag(e, content=[e.head, e.content, e.foot])
|
||||
|
||||
def generate_Figure(self, e: Figure):
|
||||
self.generate_simple_tag(e, content=[e.content, e.caption])
|
||||
|
||||
# Emtpy tags
|
||||
def generate_Null(self, e: Null):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_HorizontalRule(self, e: HorizontalRule):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_LineBreak(self, e: LineBreak):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
# Raw Block tags
|
||||
def generate_CodeBlock(self, e: CodeBlock):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
def generate_RawBlock(self, e: RawBlock):
|
||||
self.generate_simple_tag(e)
|
||||
|
||||
# Maybe move this to ImageProcessor?
|
||||
def get_image_processor_args(self, attributes:dict[str,str]) -> dict:
|
||||
# Attributes → image processor args
|
||||
additional_args = {}
|
||||
if "file-width" in attributes:
|
||||
additional_args["width"] = int(attributes["file-width"])
|
||||
if "file-height" in attributes:
|
||||
additional_args["height"] = int(attributes["file-height"])
|
||||
if "file-quality" in attributes:
|
||||
additional_args["quality"] = int(attributes["file-quality"])
|
||||
if "file-dpi" in attributes:
|
||||
additional_args["dpi"] = int(attributes["file-dpi"])
|
||||
if "file-deps" in attributes:
|
||||
additional_args["deps"] = attributes["file-deps"].split(",")
|
||||
|
||||
return additional_args
|
|
@ -1,41 +0,0 @@
|
|||
from .nop_processor import NOPProcessor
|
||||
from panflute import Div, Span, Null, Str, Plain, Quoted
|
||||
from .context import Group, InlineGroup, BlockGroup
|
||||
from .elements import FQuoted
|
||||
from .whitespace import NBSP
|
||||
from .nop_processor import ELCl
|
||||
|
||||
from typing import Callable
|
||||
|
||||
import sys
|
||||
class PandocProcessor(NOPProcessor):
|
||||
|
||||
def get_posttransformers(self) -> list[Callable[[ELCl],ELCl]]:
|
||||
return super().get_posttransformers()+[self.sanitize_attributes]
|
||||
|
||||
def sanitize_attributes(self, e: ELCl) -> ELCl:
|
||||
if hasattr(e, "attributes"):
|
||||
for key, value in e.attributes.items():
|
||||
e.attributes[key] = str(value)
|
||||
return e
|
||||
|
||||
def transform_Group(self, e: Group) -> Group:
|
||||
e.content = self.transform(e.content)
|
||||
return Div(*e.content)
|
||||
|
||||
def transform_InlineGroup(self, e: InlineGroup) -> InlineGroup:
|
||||
e.content = self.transform(e.content)
|
||||
return Span(*e.content)
|
||||
|
||||
def transform_BlockGroup(self, e: BlockGroup) -> BlockGroup:
|
||||
e.content = self.transform(e.content)
|
||||
return Div(*e.content)
|
||||
|
||||
def transform_Null(self, e: Null):
|
||||
return Plain(Str(""))
|
||||
|
||||
def transform_FQuoted(self, e: FQuoted) -> Quoted:
|
||||
return Quoted(*e.content)
|
||||
|
||||
def transform_NBSP(self, e: NBSP) -> Str:
|
||||
return Str(" ") # Unicode nbsp
|
|
@ -1,366 +0,0 @@
|
|||
from panflute import Element, ListContainer, Inline, Block
|
||||
from panflute import Cite, Code, Emph, Image, LineBreak, Link, Math, Note, Quoted, RawInline, SmallCaps, SoftBreak, Space, Span, Str, Strikeout, Strong, Subscript, Superscript, Underline
|
||||
from panflute import BlockQuote, BulletList, Citation, CodeBlock, Definition, DefinitionItem, DefinitionList, Div, Figure, Header, HorizontalRule, LineBlock, LineItem, ListItem, MetaBlocks, MetaBool, MetaInlines, MetaList, MetaMap, MetaString, Null, OrderedList, Para, Plain, RawBlock, Table, TableBody, TableFoot, TableHead
|
||||
from panflute import TableRow, TableCell, Caption, Doc
|
||||
from panflute import MetaValue
|
||||
from typing import Union
|
||||
|
||||
import os
|
||||
|
||||
from .output_generator import OutputGenerator
|
||||
from .images import ImageProcessor, ImageProcessorNamespaceSearcher
|
||||
|
||||
from .whitespace import NBSP
|
||||
from .elements import FQuoted
|
||||
from .context import Group, InlineGroup, BlockGroup, Context
|
||||
from .util import inlinify
|
||||
|
||||
class UCWTexGenerator(OutputGenerator):
|
||||
imageProcessor: ImageProcessor
|
||||
_bold: int
|
||||
_italic: int
|
||||
|
||||
def __init__(self, output_file, imageProcessor: ImageProcessor, *args, **kwargs):
|
||||
self.imageProcessor = imageProcessor
|
||||
self._bold = 0
|
||||
self._italic = 0
|
||||
super().__init__(output_file, *args, **kwargs)
|
||||
|
||||
def escape_special_chars(self, text: str) -> str:
|
||||
text = text.replace("&", r"\&")
|
||||
text = text.replace("%", r"\%")
|
||||
text = text.replace("$", r"\$")
|
||||
text = text.replace("#", r"\#")
|
||||
text = text.replace("_", r"\_")
|
||||
text = text.replace("{", r"\{")
|
||||
text = text.replace("}", r"\}")
|
||||
text = text.replace("~", r"\textasciitilde{}")
|
||||
text = text.replace("^", r"\textasciicircum{}")
|
||||
text = text.replace("\\", r"\textbackslash{}")
|
||||
text = text.replace(" ", "~") # We use unicode no-break spaces to force nbsp in output
|
||||
text = text.replace("", "")
|
||||
return text
|
||||
|
||||
def generate(self, e: Union[Element, ListContainer]):
|
||||
if hasattr(e, "attributes") and "only" in e.attributes and e.attributes["only"] != "tex":
|
||||
return
|
||||
super().generate(e)
|
||||
|
||||
def writepar(self, text: str):
|
||||
self.ensure_empty(2)
|
||||
self.writeln(text)
|
||||
self.ensure_empty(2)
|
||||
|
||||
def generate_Null(self, e: Null):
|
||||
pass
|
||||
|
||||
def generate_LineBreak(self, e: LineBreak):
|
||||
self.write(r"\\")
|
||||
self.endln()
|
||||
|
||||
def generate_Para(self, e: Para):
|
||||
self.ensure_empty(2)
|
||||
self.generate(e.content)
|
||||
self.ensure_empty(2)
|
||||
|
||||
def generate_HorizontalRule(self, e: HorizontalRule):
|
||||
self.writepar(r"\vskip5pt\hrule\hfil\vskip5pt{}")
|
||||
|
||||
def generate_Doc(self, e: Doc):
|
||||
self.writeln(r"\input ucwmac2.tex")
|
||||
self.writeln(r"\ucwmodule{ofs}")
|
||||
self.writeln(r"\ucwmodule{verb}")
|
||||
self.writeln(r"\ucwmodule{link}")
|
||||
self.writeln(r"\input formatitko.tex")
|
||||
self.generate(e.content)
|
||||
self.writeln(r"\bye")
|
||||
|
||||
def get_language_macro(self, lang: str):
|
||||
if lang == "cs":
|
||||
return r"\chyph\lefthyphenmin=2\righthyphenmin=2{}"
|
||||
elif lang == "sk":
|
||||
return r"\shyph\lefthyphenmin=2\righthyphenmin=2{}"
|
||||
elif lang == "en":
|
||||
return r"\ehyph\lefthyphenmin=2\righthyphenmin=2{}"
|
||||
else:
|
||||
return ""
|
||||
|
||||
def generate_InlineGroup(self, e: InlineGroup):
|
||||
self.write(r"{")
|
||||
self.write(self.get_language_macro(self.context.get_metadata("lang")))
|
||||
self.generate(e.content)
|
||||
self.write(r"}")
|
||||
|
||||
def generate_BlockGroup(self, e: BlockGroup):
|
||||
self.writeln(r"\begingroup")
|
||||
self.indent_more()
|
||||
self.writeln(self.get_language_macro(self.context.get_metadata("lang")))
|
||||
self.generate(e.content)
|
||||
self.indent_less()
|
||||
self.writeln(r"\endgroup")
|
||||
|
||||
def generate_Header(self, e: Header):
|
||||
self.ensure_empty(2)
|
||||
self.write("\\"+"sub"*(e.level-1)+"section{")
|
||||
self.generate(e.content)
|
||||
self.write(r"}")
|
||||
self.ensure_empty(2)
|
||||
|
||||
def generate_Image(self, e: Image):
|
||||
url = e.url
|
||||
|
||||
additional_args = self.get_image_processor_args(e.attributes)
|
||||
additional_args["context"] = self.context
|
||||
|
||||
# The directory of the current file relative to the current working directory
|
||||
source_dir = self.context.dir
|
||||
# The directory of the current file relative to the md file we were called on
|
||||
rel_dir = self.context.rel_dir
|
||||
|
||||
searcher = self.imageProcessor.get_searcher_by_path(url, rel_dir, source_dir)
|
||||
url = self.imageProcessor.get_path_without_namespace(url)
|
||||
|
||||
_, 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 = self.imageProcessor.process_image(url, ext, searcher, **additional_args)
|
||||
elif ext in ["svg"]: # FIXME
|
||||
url = self.imageProcessor.process_image(url, "pdf", searcher, **additional_args)
|
||||
elif ext in ["epdf"]:
|
||||
url = self.imageProcessor.process_image(url, "pdf", searcher, **additional_args)
|
||||
elif ext in ["jpg"]:
|
||||
url = self.imageProcessor.process_image(url, "jpeg", searcher, **additional_args)
|
||||
else:
|
||||
url = self.imageProcessor.process_image(url, "pdf", searcher, **additional_args)
|
||||
|
||||
url = searcher.get_cache_searcher().find_image(url)
|
||||
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
|
||||
|
||||
if isinstance(e.parent.parent, Figure):
|
||||
self.writeln(f'\\putimage{{{width}}}{{{url}}}')
|
||||
else:
|
||||
self.writepar(f'\\putimage{{{width}}}{{{url}}}')
|
||||
|
||||
def generate_Code(self, e: Code):
|
||||
self.write(r"\verb`")
|
||||
self.write(e.text)
|
||||
self.write(r"`")
|
||||
|
||||
def generate_Figure(self, e: Figure):
|
||||
self.ensure_empty(2)
|
||||
self.writeln(r"\vskip5pt")
|
||||
self.writeln(r"\centerline{")
|
||||
self.indent_more()
|
||||
self.generate(e.content)
|
||||
self.indent_less()
|
||||
self.writeln(r"}")
|
||||
self.writeln(r"\centerline{")
|
||||
self.indent_more()
|
||||
self.generate(e.caption)
|
||||
self.indent_less()
|
||||
self.writeln(r"}")
|
||||
self.writeln(r"\vskip5pt{}")
|
||||
self.ensure_empty(2)
|
||||
|
||||
def generate_Emph(self, e: Emph):
|
||||
if self._bold > 0:
|
||||
self.write(r"{\bi{}")
|
||||
else:
|
||||
self.write(r"{\I{}")
|
||||
self._italic+=1
|
||||
self.generate(e.content)
|
||||
self._italic-=1
|
||||
self.write(r"}")
|
||||
|
||||
def generate_Strong(self, e: Strong):
|
||||
if self._italic > 0:
|
||||
self.write(r"{\bi{}")
|
||||
else:
|
||||
self.write(r"{\bf{}")
|
||||
self._bold+=1
|
||||
self.generate(e.content)
|
||||
self._bold-=1
|
||||
self.write(r"}")
|
||||
|
||||
def generate_Caption(self, e: Caption):
|
||||
self.generate_Emph(e)
|
||||
|
||||
def generate_Math(self, e: Math):
|
||||
if e.format == "DisplayMath":
|
||||
self.ensure_empty(2)
|
||||
self.writeraw("$$")
|
||||
self.writeraw(e.text.strip())
|
||||
self.writeraw("$$")
|
||||
self.ensure_empty(2)
|
||||
else:
|
||||
self.write("$")
|
||||
self.write(e.text)
|
||||
self.write("$")
|
||||
|
||||
def generate_Note(self, e: Note):
|
||||
self.write(r"\fn{")
|
||||
self.generate(inlinify(e))
|
||||
self.write(r"}")
|
||||
|
||||
def generate_Table(self, e: Table):
|
||||
aligns = {
|
||||
"AlignLeft": r"\quad#\quad\hfil",
|
||||
"AlignRight": r"\quad\hfil#\quad",
|
||||
"AlignCenter": r"\quad\hfil#\hfil\quad",
|
||||
"AlignDefault": r"\quad#\quad\hfil"
|
||||
}
|
||||
self.writeln(r"\vskip1em")
|
||||
self.writeln(r"\halign{\strut"+"&".join([aligns[col[0]] for col in e.colspec])+r"\cr")
|
||||
self.indent_more()
|
||||
self.generate(e.head.content)
|
||||
self.writeln(r"\noalign{\hrule}")
|
||||
self.generate(e.content[0].content)
|
||||
self.writeln(r"\noalign{\hrule}")
|
||||
self.generate(e.foot.content)
|
||||
self.indent_less()
|
||||
self.writeln("}")
|
||||
self.writeln(r"\vskip1em")
|
||||
|
||||
def generate_TableRow(self, e: TableRow):
|
||||
for cell in e.content:
|
||||
if cell.colspan > 1:
|
||||
self.write(r"\multispan"+str(cell.colspan)+"{} ")
|
||||
self.generate(cell.content)
|
||||
if cell.next:
|
||||
self.write(" & ")
|
||||
self.write(r"\cr")
|
||||
self.endln()
|
||||
|
||||
def generate_RawInline(self, e: RawInline):
|
||||
if e.format == "tex":
|
||||
self.write(e.text)
|
||||
|
||||
def generate_RawBlock(self, e: RawBlock):
|
||||
if e.format == "tex":
|
||||
self.writeraw(e.text)
|
||||
|
||||
def generate_Plain(self, e: Plain):
|
||||
self.generate(e.content)
|
||||
|
||||
def generate_Span(self, e: Span):
|
||||
self.generate(e.content)
|
||||
|
||||
def generate_CodeBlock(self, e: CodeBlock):
|
||||
self.writeln(r"\verbatim{")
|
||||
self.writeraw(e.text)
|
||||
self.writeln(r"}")
|
||||
|
||||
def generate_Div(self, e: Div):
|
||||
self.generate(e.content)
|
||||
|
||||
def generate_LineBlock(self, e: LineBlock):
|
||||
self.writeln()
|
||||
self.generate(e.content)
|
||||
self.writeln()
|
||||
|
||||
def generate_LineItem(self, e: LineItem):
|
||||
self.generate(e.content)
|
||||
if e.next:
|
||||
self.write(r"\\")
|
||||
self.endln()
|
||||
|
||||
def generate_BulletList(self, e: BulletList):
|
||||
self.ensure_empty(2)
|
||||
self.writeln(r"\list{o}")
|
||||
self.indent_more()
|
||||
self.generate(e.content)
|
||||
self.indent_less()
|
||||
self.write(r"\endlist")
|
||||
self.ensure_empty(2)
|
||||
|
||||
def generate_OrderedList(self, e: OrderedList):
|
||||
self.ensure_empty(2)
|
||||
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]
|
||||
self.writeln(r"\list{"+style+r"}")
|
||||
self.indent_more()
|
||||
self.generate(e.content)
|
||||
self.indent_less()
|
||||
self.writeln(r"\endlist")
|
||||
self.ensure_empty(2)
|
||||
|
||||
def generate_ListItem(self, e: ListItem):
|
||||
self.endln()
|
||||
self.write(r"\:")
|
||||
self.generate(e.content)
|
||||
self.endln()
|
||||
|
||||
def generate_BlockQuote(self, e: BlockQuote):
|
||||
self.writeln(r"\blockquote{")
|
||||
self.indent_more()
|
||||
self.generate(e.content)
|
||||
self.indent_less()
|
||||
self.writeln(r"}")
|
||||
|
||||
def generate_Link(self, e: Link):
|
||||
if len(e.content) == 1 and isinstance(e.content[0], Str) and e.content[0].text == e.url:
|
||||
self.write(r"\url{")
|
||||
else:
|
||||
self.write(r"\linkurl{"+e.url+r"}{")
|
||||
self.generate(e.content)
|
||||
self.write(r"}") # }
|
||||
|
||||
def generate_Subscript(self, e: Subscript):
|
||||
self.write(r"\subscript{")
|
||||
self.generate(e.content)
|
||||
self.write(r"}")
|
||||
|
||||
def generate_Superscript(self, e: Superscript):
|
||||
self.write(r"\superscript{")
|
||||
self.generate(e.content)
|
||||
self.write(r"}")
|
||||
|
||||
def generate_simple_tag(self, e: Union[Element, None] = None, tag: str = "", attributes: Union[dict[str, str], None] = None, content: Union[ListContainer, Element, list[Union[Element, ListContainer]], str, None] = None, inline: Union[bool, None] = None):
|
||||
print("dumbass: ", type(e))
|
||||
|
||||
# These are also disabled in pandoc so they shouldn't appear in the AST at all.
|
||||
def generate_Citation(self, e: Citation):
|
||||
self.writeln("% FIXME: Citations not implemented")
|
||||
|
||||
def generate_Cite(self, e: Cite):
|
||||
self.writeln("% FIXME: Cites not implemented")
|
||||
|
||||
def generate_Definition(self, e: Definition):
|
||||
self.writeln("% FIXME: Definitions not implemented")
|
||||
|
||||
def generate_DefinitionItem(self, e: DefinitionItem):
|
||||
self.writeln("% FIXME: DefinitionItems not implemented")
|
||||
|
||||
def generate_DefinitionList(self, e: DefinitionList):
|
||||
self.writeln("% FIXME: DefinitionLists not implemented")
|
||||
|
||||
def generate_Underline(self, e: Underline):
|
||||
self.writeln("% FIXME: Underlines not implemented")
|
||||
|
||||
def generate_Strikeout(self, e: Strikeout):
|
||||
self.writeln("% FIXME: Strikeouts not implemented")
|
|
@ -1,308 +0,0 @@
|
|||
from panflute import Element, ListContainer, Inline, Block
|
||||
from panflute import Cite, Code, Emph, Image, LineBreak, Link, Math, Note, Quoted, RawInline, SmallCaps, SoftBreak, Space, Span, Str, Strikeout, Strong, Subscript, Superscript, Underline
|
||||
from panflute import BlockQuote, BulletList, Citation, CodeBlock, Definition, DefinitionItem, DefinitionList, Div, Figure, Header, HorizontalRule, LineBlock, LineItem, ListItem, MetaBlocks, MetaBool, MetaInlines, MetaList, MetaMap, MetaString, Null, OrderedList, Para, Plain, RawBlock, Table, TableBody, TableFoot, TableHead
|
||||
from panflute import TableRow, TableCell, Caption, Doc
|
||||
from panflute import MetaValue
|
||||
from panflute.containers import attach
|
||||
from typing import Union, Callable
|
||||
from types import ModuleType
|
||||
|
||||
import os
|
||||
import re
|
||||
import warnings
|
||||
import importlib
|
||||
import json
|
||||
|
||||
from .whitespace import NBSP
|
||||
from .elements import FQuoted
|
||||
from .context import Group, InlineGroup, BlockGroup
|
||||
from .util import nullify, import_md
|
||||
from .context import Context, CommandCallable
|
||||
from .whitespace import Whitespace, bavlna
|
||||
from .command import BlockCommand, InlineCommand, CodeCommand, Command, InlineError
|
||||
from .command_util import handle_command_define, parse_command
|
||||
from .nop_processor import NOPProcessor, ELCl, DoubleDocError
|
||||
|
||||
class TransformProcessor(NOPProcessor):
|
||||
|
||||
root_file_path: str
|
||||
root_highlight_style: str = "default"
|
||||
_command_modules: list[tuple[Union[dict[str, CommandCallable], ModuleType], str]] = []
|
||||
|
||||
class UnknownElementError(Exception):
|
||||
"An unknown Element has been passed to the TransformProcessor, probably because panflute introduced a new one."
|
||||
pass
|
||||
|
||||
def __init__(self, root_file_path: str, *args, **kwargs):
|
||||
self.root_file_path = root_file_path
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def add_command_module(self, module: Union[dict[str, CommandCallable], ModuleType], module_name: str=""):
|
||||
self._command_modules.append((module, module_name))
|
||||
|
||||
def init_context(self, e: Doc) -> Context:
|
||||
if self.context is not None:
|
||||
raise DoubleDocError()
|
||||
self.context = Context(e, self.root_file_path)
|
||||
for module, module_name in self._command_modules:
|
||||
self.context.add_commands_from_module(module, module_name)
|
||||
e.content = [BlockGroup(*e.content, context=self.context)]
|
||||
return self.context
|
||||
|
||||
def get_pretransformers(self) -> list[Callable[[ELCl],ELCl]]:
|
||||
return super().get_pretransformers()+[self.handle_if_attribute, self.handle_ifnot_attribute]
|
||||
|
||||
def handle_if_attribute(self, e: ELCl) -> ELCl:
|
||||
# `if` attribute. Only show this element if flag is set.
|
||||
if hasattr(e, "attributes") and "if" in e.attributes:
|
||||
if not self.context.is_flag_set(e.attributes["if"]):
|
||||
return nullify(e)
|
||||
return e
|
||||
|
||||
def handle_ifnot_attribute(self, e: ELCl) -> ELCl:
|
||||
# `ifnot` attribute. Only show this element if flag is NOT set
|
||||
if hasattr(e, "attributes") and "ifnot" in e.attributes:
|
||||
if self.context.is_flag_set(e.attributes["ifnot"]):
|
||||
return nullify(e)
|
||||
return e
|
||||
|
||||
def transform_ListContainer(self, e: ListContainer) -> ListContainer:
|
||||
try:
|
||||
return super().transform_ListContainer(e)
|
||||
except TypeError as err:
|
||||
names = []
|
||||
for el in e:
|
||||
if hasattr(el, "attributes") and "c" in el.attributes:
|
||||
names.append(el.attributes["c"])
|
||||
if len(names) > 0:
|
||||
raise InlineError(f"The command{'s' if len(names) > 1 else ''} {names[0] if len(names) == 1 else names} was called in an Inline way but returned Block content. Put it in a paragraph alone or execute it as a Div using: \n::: {{c={names[0] if len(names) == 1 else '<command_name>'}}}\n:::")
|
||||
else:
|
||||
raise err
|
||||
|
||||
def transform_Doc(self, e: Doc) -> Doc:
|
||||
self.init_context(e)
|
||||
e.content = self.transform(e.content)
|
||||
return e
|
||||
|
||||
|
||||
def transform_Quoted(self, e: Quoted) -> FQuoted:
|
||||
e.content = self.transform(e.content)
|
||||
quote_styles = {
|
||||
"cs": "cs",
|
||||
"en": "en",
|
||||
"sk": "cs",
|
||||
None: None
|
||||
}
|
||||
return FQuoted(*e.content, quote_type=e.quote_type, style=quote_styles[self.context.get_metadata("lang")])
|
||||
|
||||
def transform_Image(self, e: Image) -> Image:
|
||||
e.content = self.transform(e.content)
|
||||
# OG now has Context so this is not needed per se, but I'm keeping this here for the handling of attribute > context > default value
|
||||
# Pass down "no-srcset" metadatum as attribute down to images.
|
||||
if "no-srcset" not in e.attributes:
|
||||
e.attributes["no-srcset"] = self.context.get_metadata("no-srcset") if self.context.get_metadata("no-srcset") is not None else False
|
||||
if "no-img-link" not in e.attributes:
|
||||
e.attributes["no-img-link"] = self.context.get_metadata("no-img-link") if self.context.get_metadata("no-img-link") is not None else False
|
||||
return e
|
||||
|
||||
def create_Group(self, *content, new_context: Context, replaced:Element, inline: bool=False) -> Group:
|
||||
old_context = self.context
|
||||
self.context = new_context
|
||||
if inline:
|
||||
g = InlineGroup(*content, context=new_context)
|
||||
else:
|
||||
g = BlockGroup(*content, context=new_context)
|
||||
attach(g, replaced.parent, replaced.location, replaced.index)
|
||||
g = self.transform(g)
|
||||
self.context = old_context
|
||||
return g
|
||||
|
||||
def transform_Para(self, e: Para) -> Union[Para, Div]:
|
||||
if len(e.content) == 1 and isinstance(e.content[0], Span):
|
||||
# If the span turns out to be a command, it might return a Div. We should then replace ourselves with the Div
|
||||
span = e.content[0]
|
||||
span = self.transform(span)
|
||||
if isinstance(span, Div):
|
||||
return span
|
||||
else:
|
||||
e.content[0] = span
|
||||
return super().transform_Para(e)
|
||||
else:
|
||||
return super().transform_Para(e)
|
||||
|
||||
def transform_Div(self, e: Div) -> Union[Div, Group, Null, RawBlock]:
|
||||
|
||||
if "group" in e.classes:
|
||||
# `.group` class for Divs
|
||||
# Content of Div is enclosed in a separate context, all attributes are passed as metadata
|
||||
new_context = Context(Doc(), self.context.path, self.context, trusted=self.context.trusted)
|
||||
for attribute, value in e.attributes.items():
|
||||
new_context.set_metadata(attribute, value)
|
||||
return self.create_Group(*e.content, replaced=e, new_context=new_context)
|
||||
|
||||
if "c" in e.attributes:
|
||||
# Commands can be called multiple ways, this handles the following syntax:
|
||||
# :::{c=commandname}
|
||||
# :::
|
||||
command = BlockCommand(*e.content, identifier=e.identifier, classes=e.classes, attributes=e.attributes)
|
||||
attach(command, e.parent, e.location, e.index)
|
||||
return self.transform(command)
|
||||
|
||||
if "partial" in e.attributes:
|
||||
# `partial` attribute
|
||||
# Used to include a file which is executed in a different (child) Context.
|
||||
if not "type" in e.attributes:
|
||||
e.attributes["type"] = "md"
|
||||
if not self.context.trusted: # If we're in an untrusted context, we shouldn't allow inclusion of files outside the PWD.
|
||||
full_path = os.path.abspath(self.context.dir + "/" + e.attributes["partial"])
|
||||
pwd = os.path.abspath(".")
|
||||
if os.path.commonpath([full_path, pwd]) != os.path.commonpath([pwd]):
|
||||
return nullify(e)
|
||||
filename = self.context.dir + "/" + e.attributes["partial"]
|
||||
self.context.add_dep(filename)
|
||||
text = open(filename, "r").read()
|
||||
path = self.context.dir + "/" + e.attributes["partial"]
|
||||
if e.attributes["type"] == "md":
|
||||
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 self.context.trusted:
|
||||
trusted = False
|
||||
return self.create_Group(*includedDoc.content, replaced=e, new_context=Context(includedDoc, path, self.context, trusted=trusted))
|
||||
elif e.attributes["type"] in ["tex", "html"]:
|
||||
return RawBlock(text, e.attributes["type"])
|
||||
|
||||
if "header_content" in e.classes:
|
||||
header_content = self.context.get_metadata("header_content")
|
||||
header_content = [] if header_content is None else header_content
|
||||
header_content.append(MetaBlocks(*self.transform(e.content)))
|
||||
self.context.set_metadata("header_content", header_content)
|
||||
return Null()
|
||||
|
||||
if "footer_content" in e.classes:
|
||||
footer_content = self.context.get_metadata("footer_content")
|
||||
footer_content = [] if footer_content is None else footer_content
|
||||
footer_content.append(MetaBlocks(*self.transform(e.content)))
|
||||
self.context.set_metadata("footer_content", footer_content)
|
||||
return Null()
|
||||
|
||||
if "lang" in e.attributes:
|
||||
warnings.warn("To set language in a way formátítko will understand, this Div has to have the `.group` class and be a Group.", UserWarning)
|
||||
|
||||
return super().transform_Div(e)
|
||||
|
||||
def transform_Span(self, e: Span) -> Span:
|
||||
|
||||
if "group" in e.classes:
|
||||
# `.group` class for Spans
|
||||
# Content of Span is enclosed in a separate context, all attributes are passed as metadata
|
||||
new_context = Context(Doc(), self.context.path, self.context, trusted=self.context.trusted)
|
||||
for attribute, value in e.attributes.items():
|
||||
new_context.set_metadata(attribute, value)
|
||||
return self.create_Group(*e.content, replaced=e, new_context=new_context, inline=True)
|
||||
|
||||
if "c" in e.attributes:
|
||||
# Commands can be called multiple ways, this handles the following syntax:
|
||||
# []{c=commandname} and
|
||||
command = InlineCommand(*e.content, identifier=e.identifier, classes=e.classes, attributes=e.attributes)
|
||||
attach(command, e.parent, e.location, e.index)
|
||||
return self.transform(command)
|
||||
|
||||
if len(e.content) == 1 and isinstance(e.content[0], Str):
|
||||
## Handle special command shorthand [!commandname]{}
|
||||
if re.match(r"^![\w.]+$", e.content[0].text):
|
||||
command = InlineCommand(identifier=e.identifier, classes=e.classes, attributes={**e.attributes, "c": e.content[0].text[1:]})
|
||||
attach(command, e.parent, e.location, e.index)
|
||||
return self.transform(command)
|
||||
|
||||
## Handle import [#ksp_formatitko as ksp]{}, [#ksp_formatitko]{type=module} or [#path/file.md]{type=md}
|
||||
# Import a python module as commands (type=module, the default) or
|
||||
# import all metadata from a md file, dropping its contents.
|
||||
elif re.match(r"^#.+$", e.content[0].text):
|
||||
if not "type" in e.attributes:
|
||||
e.attributes["type"] = "module"
|
||||
if e.attributes["type"] == "md":
|
||||
filename = self.context.dir + "/" + e.content[0].text[1:]
|
||||
self.context.add_dep(filename)
|
||||
importedDoc = import_md(open(filename, "r").read())
|
||||
self.transform(importedDoc.content)
|
||||
elif e.attributes["type"] == "module":
|
||||
matches = re.match(r"^(\w+)(?: as (\w+))?$", e.content[0].text[1:])
|
||||
if not matches:
|
||||
raise SyntaxError(f"`{e.content[0].text[1:]}`: invalid syntax")
|
||||
module = importlib.import_module(matches.group(1))
|
||||
module_name = matches.group(1) if matches.group(2) is None else matches.group(2)
|
||||
self.context.add_commands_from_module(module, module_name)
|
||||
elif e.attributes["type"] == "metadata":
|
||||
filename = self.context.dir + "/" + e.content[0].text[1:]
|
||||
self.context.add_dep(filename)
|
||||
data = json.load(open(filename, "r"))
|
||||
key = "" if not "key" in e.attributes else e.attributes["key"]
|
||||
self.context.import_metadata(data, key)
|
||||
else:
|
||||
raise SyntaxError(f"`{e.attributes['type']}`: invalid import type")
|
||||
|
||||
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 = self.context.get_metadata(e.content[0].text[1:], False)
|
||||
if isinstance(val, MetaInlines): # TODO: Trust transform for this
|
||||
e = Span(*val.content)
|
||||
e = self.transform(e)
|
||||
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)}'")
|
||||
return e
|
||||
|
||||
return super().transform_Span(e)
|
||||
|
||||
def transform_CodeBlock(self, e: CodeBlock) -> Union[CodeBlock, Div, Null]:
|
||||
if "markdown" in e.classes and "group" in e.classes:
|
||||
includedDoc = import_md(e.text)
|
||||
return self.create_Group(*includedDoc.content, replaced=e, new_context=Context(includedDoc, self.context.path, self.context, self.context.trusted))
|
||||
|
||||
if "python" in e.classes and "run" in e.classes:
|
||||
if not self.context.trusted:
|
||||
return nullify(e)
|
||||
command_output = parse_command(e.text)(BlockCommand(), self.context, self)
|
||||
command = BlockCommand().replaceSelf(*([] if command_output is None else command_output))
|
||||
attach(command, e.parent, e.location, e.index)
|
||||
return self.transform(command)
|
||||
|
||||
if "python" in e.classes and ("define" in e.attributes or "redefine" in e.attributes):
|
||||
if not self.context.trusted:
|
||||
return nullify(e)
|
||||
return handle_command_define(e, self.context)
|
||||
|
||||
if "c" in e.attributes:
|
||||
command = CodeCommand(e.text, identifier=e.identifier, classes=e.classes, attributes=e.attributes)
|
||||
attach(command, e.parent, e.location, e.index)
|
||||
return self.transform(command)
|
||||
|
||||
# Pass down metadata 'highlight' and 'highlight_style' as attribute to CodeBlocks
|
||||
# OG now has Context so this is not needed per se, but I'm keeping this here for the handling of attribute > context > default value
|
||||
if not "highlight" in e.attributes:
|
||||
e.attributes["highlight"] = self.context.get_metadata("highlight") if self.context.get_metadata("highlight") is not None else True
|
||||
if not "style" in e.attributes:
|
||||
e.attributes["style"] = self.context.get_metadata("highlight-style") if self.context.get_metadata("highlight-style") is not None else "default"
|
||||
return e
|
||||
|
||||
def transform_Command(self, e: Command) -> Union[Div, Span]:
|
||||
if not self.context.get_command(e.attributes["c"]):
|
||||
raise NameError(f"Command not defined '{e.attributes['c']}'.")
|
||||
command_output = self.context.get_command(e.attributes["c"])(e, self.context, self)
|
||||
e = e.replaceSelf(*([] if command_output is None else command_output))
|
||||
return e
|
||||
|
||||
def transform_Whitespace(self, e: Whitespace) -> Whitespace:
|
||||
if bavlna(e, self.context):
|
||||
return NBSP()
|
||||
else:
|
||||
return e
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
# H1
|
||||
## H2
|
||||
### H3
|
||||
#### H4
|
||||
##### H5
|
||||
###### H6
|
||||
|
||||
---
|
|
@ -1,2 +0,0 @@
|
|||
import graph;
|
||||
draw(Circle((0,0),20)); // graph - Circle
|
|
@ -1,10 +1,7 @@
|
|||
```python {define=nop}
|
||||
return element.content
|
||||
appendChildren(element.content)
|
||||
```
|
||||
|
||||
```python {define=opendatatask}
|
||||
return import_md_list("Toto je praktická open-data úloha. V [odevzdávátku](https://ksp.mff.cuni.cz/h/odevzdavatko/) si necháte vygenerovat vstupy a odevzdáte příslušné výstupy. Záleží jen na vás, jak výstupy vyrobíte.")
|
||||
```
|
||||
```python {define=opendatatask2}
|
||||
return [*parse_string("Toto je praktická open-data úloha. V "),pf.Link(pf.Str("odevzdávátku"), url="https://ksp.mff.cuni.cz/h/odevzdavatko/"),*parse_string(" si necháte vygenertovat vstupy a odevzdáte příslušné výstupy. Záleží jen na vás, jak výstupy vyrobíte.")]
|
||||
println("Toto je praktická open-data úloha. V [odevzdávátku](https://ksp.mff.cuni.cz/h/odevzdavatko/) si necháte vygenerovat vstupy a odevzdáte příslušné výstupy. Záleží jen na vás, jak výstupy vyrobíte.")
|
||||
```
|
||||
|
|
|
@ -11,33 +11,27 @@ And things...
|
|||
|
||||
:::
|
||||
|
||||
```python {.run}
|
||||
``` {.python .run}
|
||||
# I set my own flags!
|
||||
context.set_flag("cat", True)
|
||||
ctx.set_flag("cat", True)
|
||||
```
|
||||
|
||||
```python {.run}
|
||||
return import_md_list(f"The subdocument's title is\n\n# {context.get_metadata('title')}\n\nThe subdocument's subtitle is\n\n## {context.get_metadata('subtitle')}")
|
||||
```
|
||||
```python {.run}
|
||||
return [
|
||||
pf.Para(*parse_string("The subdocument's title is")),
|
||||
pf.Header(*parse_string(context.get_metadata('title')), level=1),
|
||||
pf.Para(*parse_string("The subdocument's subtitle is")),
|
||||
pf.Header(*parse_string(context.get_metadata('subtitle')), level=2)
|
||||
]
|
||||
``` {.python .run}
|
||||
println(f"The subdocument's title is \n\n# {ctx.get_metadata('title')}")
|
||||
println()
|
||||
println(f"The subdocument's subtitle is \n\n## {ctx.get_metadata('subtitle')}")
|
||||
```
|
||||
|
||||
```markdown {.group}
|
||||
---
|
||||
lang: "cs"
|
||||
language: "cs"
|
||||
---
|
||||
Tak toto je "v prádelně" pánové!
|
||||
```
|
||||
|
||||
```markdown {.group}
|
||||
---
|
||||
lang: "en"
|
||||
language: "en"
|
||||
---
|
||||
This is "in a laundry room" gentlemen!
|
||||
```
|
||||
|
@ -48,7 +42,6 @@ I am a duck.
|
|||
This should be only shown to included cats.
|
||||
:::
|
||||
|
||||
![A circle diagram](circle.asy){width=50px}
|
||||
|
||||
$$
|
||||
\def\eqalign#1{NO, just, nooooo}
|
||||
|
@ -56,16 +49,7 @@ $$
|
|||
$$
|
||||
|
||||
|
||||
<!--There is an inline *emphasis with $math \error$*.-->
|
||||
|
||||
<!--
|
||||
```python {.run}
|
||||
print("bruh")
|
||||
raise Exception("Jsem piča")
|
||||
```
|
||||
-->
|
||||
|
||||
![This is a figure, go figure...](logo.svg){width=25%}What
|
||||
![This is a figure, go figure...](logo.svg){width=25%}
|
||||
|
||||
![This is a figure, go figure...](logo.pdf){width=50%}
|
||||
|
||||
|
|
8
test/test-top.html
Normal file
8
test/test-top.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
<!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>
|
|
@ -1,22 +0,0 @@
|
|||
[
|
||||
{
|
||||
"first_name": "Jan",
|
||||
"last_name": "Černohorský",
|
||||
"abbrev": "GS",
|
||||
"gender": "M",
|
||||
"contacts_public": {
|
||||
"homepage": "https://grsc.cz"
|
||||
}
|
||||
},
|
||||
{
|
||||
"first_name": "Ondřej",
|
||||
"last_name": "Machota",
|
||||
"domestic_name": "Ondra",
|
||||
"abbrev": "OM",
|
||||
"gender": "M",
|
||||
"contacts_public": {
|
||||
"email": "ondrejmachota@gmail.com",
|
||||
"discord": "ondrejmachota#0196"
|
||||
}
|
||||
}
|
||||
]
|
83
test/test.md
83
test/test.md
|
@ -2,28 +2,11 @@
|
|||
title: 'Wooooo a title'
|
||||
subtitle: 'A subtitle'
|
||||
are_we_there_yet: False
|
||||
lang: "en"
|
||||
header-includes: |
|
||||
<style>
|
||||
body {
|
||||
color: forestgreen;
|
||||
}
|
||||
</style>
|
||||
language: "en"
|
||||
---
|
||||
[#test-files/test-import.md]{type=md}
|
||||
|
||||
[#test.json]{type=metadata key=orgs}
|
||||
|
||||
```python {.run}
|
||||
return parse_string(f"Hello, {context.get_metadata('orgs')[0]['first_name']}!")
|
||||
```
|
||||
[#test-files/test-import.md]{}
|
||||
|
||||
# Hello world!
|
||||
## H2
|
||||
### H3
|
||||
#### H4
|
||||
##### H5
|
||||
###### H6
|
||||
|
||||
This is an *example* **yay**!
|
||||
|
||||
|
@ -42,14 +25,14 @@ This should only be shown to cats
|
|||
:::
|
||||
|
||||
```python {.run}
|
||||
context.set_flag("cat", True)
|
||||
ctx.set_flag("cat", True)
|
||||
```
|
||||
|
||||
```python {.run}
|
||||
context.set_metadata("a", {})
|
||||
context.set_metadata("a.b", {})
|
||||
context.set_metadata("a.b.c", "Bruh **bruh** bruh")
|
||||
return [*parse_string("The main document's title is "), pf.Quoted(*parse_string(context.get_metadata('title')), quote_type="SingleQuote"), pf.Str(".")]
|
||||
println(f"The main document's title is '{ctx.get_metadata('title')}'")
|
||||
ctx.set_metadata("a", {})
|
||||
ctx.set_metadata("a.b", {})
|
||||
ctx.set_metadata("a.b.c", "Bruh **bruh** bruh")
|
||||
```
|
||||
|
||||
```python {style=native}
|
||||
|
@ -57,11 +40,6 @@ def bruh(no):
|
|||
wat
|
||||
```
|
||||
|
||||
```python {style=dracula}
|
||||
def bruhec(bruzek):
|
||||
hah
|
||||
```
|
||||
|
||||
Inline `code`
|
||||
|
||||
::::{if=cat}
|
||||
|
@ -72,7 +50,7 @@ This should only be shown to cats the second time
|
|||
|
||||
```markdown {.group}
|
||||
---
|
||||
lang: cs
|
||||
language: cs
|
||||
---
|
||||
V​ pravém jízdním bruhu.
|
||||
V pravém jízdním bruhu.
|
||||
|
@ -80,7 +58,6 @@ V pravém jízdním bruhu.
|
|||
V pravém jízdním bruhu.
|
||||
|
||||
[!opendatatask]{}
|
||||
[!opendatatask2]{}
|
||||
```
|
||||
|
||||
[This too!]{if=cat}
|
||||
|
@ -91,8 +68,6 @@ V pravém jízdním bruhu.
|
|||
|
||||
[!nop]{a=b}<!-- A special command! WOW -->
|
||||
|
||||
[Hehehehe čeština bružek]{lang=cs}
|
||||
|
||||
> OOO a blockquote mate init
|
||||
>
|
||||
>> Nesting??
|
||||
|
@ -108,8 +83,6 @@ A link with the link in the link: <https://bruh.com>
|
|||
|
||||
H~2~O is a liquid. 2^10^ is 1024.
|
||||
|
||||
[Kuchařka](ksp:///kucharky/uvodni)
|
||||
|
||||
[Underline]{.underline}
|
||||
|
||||
:::{only=html}
|
||||
|
@ -131,17 +104,6 @@ $$
|
|||
|
||||
---
|
||||
|
||||
In this text, there might be some phrases [v češtině]{.group lang=cs} and
|
||||
maybe even
|
||||
|
||||
:::{.group lang=cs}
|
||||
celé pasáže textu v češtině.
|
||||
|
||||
Růžový bagr bez zeleného bagru se žlutým bagrem.
|
||||
|
||||
Yay můžeme mít izolované kontexty bez opakovaného volání pandocu jupí!
|
||||
:::
|
||||
|
||||
This should be seen by all.^[This is a footnote]
|
||||
|
||||
| Matematicko-fyzikální fakulta University Karlovy
|
||||
|
@ -196,32 +158,3 @@ ii. wym bro
|
|||
1 1 1 1
|
||||
------- ------ ---------- -------
|
||||
|
||||
```python {define=bash}
|
||||
import subprocess
|
||||
c = subprocess.run(["bash", "-c", element.text], stdout=subprocess.PIPE, check=True, encoding="utf-8")
|
||||
return [pf.CodeBlock(c.stdout)]
|
||||
```
|
||||
|
||||
```bash {c=bash}
|
||||
cat /etc/os-release
|
||||
```
|
||||
|
||||
::: {.group lang=cs}
|
||||
```python {.run}
|
||||
return processor.transform([
|
||||
*parse_string("V "),
|
||||
pf.Link(pf.Str("odevzdávátku"), url="https://ksp.mff.cuni.cz/z/odevzdavatko/"),
|
||||
*parse_string(" si necháte vygenerovat vstupy a odevzdáte příslušné výstupy. Záleží jen na vás, jak výstupy vyrobíte.")
|
||||
|
||||
])
|
||||
```
|
||||
:::
|
||||
|
||||
```html
|
||||
<div>
|
||||
hahahahaah
|
||||
</div>
|
||||
|
||||
```
|
||||
|
||||
`<div>`
|
||||
|
|
267
tex.py
Normal file
267
tex.py
Normal file
|
@ -0,0 +1,267 @@
|
|||
from panflute import *
|
||||
import os
|
||||
|
||||
from whitespace import NBSP
|
||||
from transform 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: Element, 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 hasattr(e, "level") 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
|
||||
|
||||
# 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 output
|
||||
# directory.
|
||||
url = i.process_image(url, ext, source_dir, relative=False, **additional_args)
|
||||
elif ext in ["svg"]:
|
||||
url = i.process_image(url, "pdf", source_dir, relative=False, **additional_args)
|
||||
elif ext in ["epdf"]:
|
||||
url = i.process_image(url, "pdf", source_dir, relative=False, **additional_args)
|
||||
elif ext in ["jpg"]:
|
||||
url = i.process_image(url, "jpeg", source_dir, relative=False, **additional_args)
|
||||
else:
|
||||
url = i.process_image(url, "pdf", source_dir, relative=False, **additional_args)
|
||||
|
||||
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 "language" in e.metadata and e.metadata["language"] is not None:
|
||||
open = "\\language"+e.metadata["language"]
|
||||
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
|
175
transform.py
Normal file
175
transform.py
Normal file
|
@ -0,0 +1,175 @@
|
|||
from panflute import *
|
||||
import re
|
||||
|
||||
# Import local files
|
||||
from whitespace import *
|
||||
from command import *
|
||||
from util import *
|
||||
from context import *
|
||||
|
||||
|
||||
# This is a small extension to the Quoted panflute elements which allows to
|
||||
# have language-aware quotation marks.
|
||||
class FQuoted(Quoted):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.style = kwargs["style"]
|
||||
del kwargs["style"]
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
# 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:
|
||||
|
||||
# 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("language")
|
||||
includedDoc = includedDoc.walk(transform, nContext)
|
||||
e = Group(*includedDoc.content, metadata={"language": 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("language")])
|
||||
|
||||
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
|
||||
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)
|
||||
e = Div(*executeCommand(e.text, None, c))
|
||||
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']}'.")
|
||||
e = e.replaceSelf(executeCommand(c.get_command(e.attributes["c"]), e, c))
|
||||
e = e.walk(transform, c)
|
||||
|
||||
return e
|
|
@ -1,11 +1,10 @@
|
|||
from panflute import Element, Block, Inline, Null, Str, Doc, convert_text, Para, Plain, Span, Space
|
||||
from panflute import Element, Block, Inline, Null, Str, Doc, convert_text, Para, Plain
|
||||
import re
|
||||
from typing import Union
|
||||
|
||||
# It sometimes happens that an element contains a single paragraph or even a
|
||||
# single plaintext line. It can be sometimes useful to extract this single
|
||||
# paragraph, which is inline.
|
||||
def inlinify(e: Element) -> Union[list[Element], None]:
|
||||
def inlinify(e: Element) -> Element:
|
||||
if len(e.content) == 1 and (isinstance(e.content[0], Para) or isinstance(e.content[0], Plain)):
|
||||
return e.content[0].content
|
||||
|
||||
|
@ -13,31 +12,14 @@ def inlinify(e: Element) -> Union[list[Element], None]:
|
|||
# cannot be removed from the tree entirely, because that would mess up the
|
||||
# iteration process through the tree. We replace them with null elements
|
||||
# instead which never make it to the output.
|
||||
def nullify(e: Element) -> Union[Str, Null]:
|
||||
def nullify(e: Element):
|
||||
if isinstance(e, Inline):
|
||||
return Str("")
|
||||
else:
|
||||
elif isinstance(e, Block):
|
||||
return Null()
|
||||
|
||||
|
||||
def parse_string(s: str) -> list[Union[Str, Space]]:
|
||||
words = s.split(" ")
|
||||
output = []
|
||||
first_word, *words = words
|
||||
if first_word != "":
|
||||
output.append(Str(first_word))
|
||||
for word in words:
|
||||
output.append(Space())
|
||||
if word != "":
|
||||
output.append(Str(word))
|
||||
return output
|
||||
|
||||
|
||||
# A helper function to import markdown using panflute (which calls pandoc). If
|
||||
# we ever want to disable or enable some of panflute's markdown extensions,
|
||||
# this is the place to do it.
|
||||
def import_md(s: str, standalone: bool=True) -> Union[Doc, list[Element]]:
|
||||
return convert_text(s, standalone=standalone, input_format="markdown-definition_lists-latex_macros", extra_args=["--strip-comments"])
|
||||
|
||||
def import_md_list(s: str) -> list[Element]:
|
||||
return import_md(s, standalone=False)
|
||||
def import_md(s: str, standalone: bool=True) -> Doc:
|
||||
return convert_text(s, standalone=standalone, input_format="markdown-definition_lists-citations")
|
|
@ -2,7 +2,7 @@ from panflute import Space, SoftBreak, Str, Math
|
|||
from typing import Union
|
||||
|
||||
# Import local files
|
||||
from .context import Context
|
||||
from context import Context
|
||||
|
||||
Whitespace = Union[Space,SoftBreak]
|
||||
|
||||
|
@ -12,8 +12,8 @@ class NBSP(Space):
|
|||
# This function tries to determine if a space should be non-breaking. It is
|
||||
# language-aware and tries to be sort-of smart about its decisions.
|
||||
def bavlna(e: Whitespace, c: Context) -> bool:
|
||||
|
||||
if c.get_metadata("lang") == "cs":
|
||||
|
||||
if c.get_metadata("language") == "cs":
|
||||
# Add no-break space after single letter prepositions and conjunctions.
|
||||
# Also tries to find them inside elements, for instance
|
||||
# `V [odevzdávátku]()` should get correctly detected.
|
||||
|
@ -36,9 +36,9 @@ def bavlna(e: Whitespace, c: Context) -> bool:
|
|||
if prevC in operators and nextC in numbers:
|
||||
return True
|
||||
|
||||
# if isinstance(e.prev, Math) or isinstance(e.next, Math):
|
||||
# # Add no-break spaces around TeX math.
|
||||
# return True
|
||||
if isinstance(e.prev, Math) or isinstance(e.next, Math):
|
||||
# Add no-break spaces around TeX math.
|
||||
return True
|
||||
|
||||
|
||||
|
Loading…
Reference in a new issue