typstpy 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- typstpy/__init__.py +18 -0
- typstpy/functions.py +269 -0
- typstpy/param_types.py +178 -0
- typstpy/utils.py +88 -0
- typstpy-0.0.1.dist-info/LICENSE.txt +21 -0
- typstpy-0.0.1.dist-info/METADATA +71 -0
- typstpy-0.0.1.dist-info/RECORD +8 -0
- typstpy-0.0.1.dist-info/WHEEL +4 -0
typstpy/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from .functions import emph, figure, heading, image, par, strong, text
|
|
2
|
+
from .param_types import Block, Content, Label, Length, Ratio, Relative
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"emph",
|
|
6
|
+
"figure",
|
|
7
|
+
"heading",
|
|
8
|
+
"image",
|
|
9
|
+
"par",
|
|
10
|
+
"strong",
|
|
11
|
+
"text",
|
|
12
|
+
"Block",
|
|
13
|
+
"Content",
|
|
14
|
+
"Label",
|
|
15
|
+
"Length",
|
|
16
|
+
"Ratio",
|
|
17
|
+
"Relative",
|
|
18
|
+
]
|
typstpy/functions.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from cytoolz.curried import assoc, valfilter # type:ignore
|
|
4
|
+
from pymonad.reader import Pipe # type:ignore
|
|
5
|
+
|
|
6
|
+
from .param_types import Block, Content, Label, Length, Relative
|
|
7
|
+
from .utils import RenderType, attach_func, render
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def text(
|
|
11
|
+
content: Block,
|
|
12
|
+
*,
|
|
13
|
+
font: Optional[str | tuple[str]] = None,
|
|
14
|
+
fallback: Optional[bool] = None,
|
|
15
|
+
size: Optional[Length] = None,
|
|
16
|
+
) -> Block:
|
|
17
|
+
"""Interface of `text` function in typst.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
content (Block): Content in which all text is styled according to the other arguments.
|
|
21
|
+
font (Optional[str | tuple[str]], optional): A font family name or priority list of font family names. Defaults to None.
|
|
22
|
+
fallback (Optional[bool], optional): Whether to allow last resort font fallback when the primary font list contains no match. This lets Typst search through all available fonts for the most similar one that has the necessary glyphs. Defaults to None.
|
|
23
|
+
size (Optional[Length], optional): The size of the glyphs. Defaults to None.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Block: Executable typst block.
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
>>> text("Hello, World!", font="Arial", fallback=True)
|
|
30
|
+
'#text(font: "Arial", fallback: true)[Hello, World!]'
|
|
31
|
+
>>> text("Hello, World!", font=("Arial", "Times New Roman"), fallback=True)
|
|
32
|
+
'#text(font: ("Arial", "Times New Roman"), fallback: true)[Hello, World!]'
|
|
33
|
+
>>> text("Hello, World!", size=Length(12, "pt"))
|
|
34
|
+
'#text(size: 12pt)[Hello, World!]'
|
|
35
|
+
"""
|
|
36
|
+
params = (
|
|
37
|
+
Pipe({"font": font, "fallback": fallback, "size": size})
|
|
38
|
+
.map(valfilter(lambda x: x is not None))
|
|
39
|
+
.flush()
|
|
40
|
+
)
|
|
41
|
+
if not params:
|
|
42
|
+
return content
|
|
43
|
+
return rf"#text({render(RenderType.DICT)(params)})[{content}]"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def emph(content: Block) -> Block:
|
|
47
|
+
"""Interface of `emph` function in typst.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
content (Block): The content to emphasize.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Block: Executable typst block.
|
|
54
|
+
|
|
55
|
+
Examples:
|
|
56
|
+
>>> emph("Hello, World!")
|
|
57
|
+
'#emph[Hello, World!]'
|
|
58
|
+
"""
|
|
59
|
+
return rf"#emph[{content}]"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def strong(content: Block, *, delta: Optional[int] = None) -> Block:
|
|
63
|
+
"""Interface of `strong` function in typst.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
content (Block): The content to strongly emphasize.
|
|
67
|
+
delta (Optional[int], optional): The delta to apply on the font weight. Defaults to None.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Block: Executable typst block.
|
|
71
|
+
|
|
72
|
+
Examples:
|
|
73
|
+
>>> strong("Hello, World!")
|
|
74
|
+
'#strong[Hello, World!]'
|
|
75
|
+
>>> strong("Hello, World!", delta=300)
|
|
76
|
+
'#strong(delta: 300)[Hello, World!]'
|
|
77
|
+
"""
|
|
78
|
+
params = Pipe({"delta": delta}).map(valfilter(lambda x: x is not None)).flush()
|
|
79
|
+
if not params:
|
|
80
|
+
return rf"#strong[{content}]"
|
|
81
|
+
return rf"#strong({render(RenderType.DICT)(params)})[{content}]"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def par(
|
|
85
|
+
content: Block,
|
|
86
|
+
*,
|
|
87
|
+
leading: Optional[Length] = None,
|
|
88
|
+
justify: Optional[bool] = None,
|
|
89
|
+
linebreaks: Optional[str] = None,
|
|
90
|
+
first_line_indent: Optional[Length] = None,
|
|
91
|
+
hanging_indent: Optional[Length] = None,
|
|
92
|
+
) -> Block:
|
|
93
|
+
"""Interface of `par` function in typst.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
content (Block): The contents of the paragraph.
|
|
97
|
+
leading (Optional[Length], optional): The spacing between lines. Defaults to None.
|
|
98
|
+
justify (Optional[bool], optional): Whether to justify text in its line. Defaults to None.
|
|
99
|
+
linebreaks (Optional[str], optional): How to determine line breaks. Options are "simple" and "optimized". Defaults to None.
|
|
100
|
+
first_line_indent (Optional[Length], optional): The indent the first line of a paragraph should have. Defaults to None.
|
|
101
|
+
hanging_indent (Optional[Length], optional): The indent all but the first line of a paragraph should have. Defaults to None.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Block: Executable typst block.
|
|
105
|
+
|
|
106
|
+
Examples:
|
|
107
|
+
>>> par("Hello, World!", leading=Length(1.5, "em"))
|
|
108
|
+
'#par(leading: 1.5em)[Hello, World!]'
|
|
109
|
+
>>> par("Hello, World!", justify=True)
|
|
110
|
+
'#par(justify: true)[Hello, World!]'
|
|
111
|
+
>>> par("Hello, World!")
|
|
112
|
+
'Hello, World!'
|
|
113
|
+
"""
|
|
114
|
+
if linebreaks and linebreaks not in ("simple", "optimized"):
|
|
115
|
+
raise ValueError(f"Invalid value for linebreaks: {linebreaks}.")
|
|
116
|
+
params = (
|
|
117
|
+
Pipe(
|
|
118
|
+
{
|
|
119
|
+
"leading": leading,
|
|
120
|
+
"justify": justify,
|
|
121
|
+
"linebreaks": linebreaks,
|
|
122
|
+
"first_line_indent": first_line_indent,
|
|
123
|
+
"hanging_indent": hanging_indent,
|
|
124
|
+
}
|
|
125
|
+
)
|
|
126
|
+
.map(valfilter(lambda x: x is not None))
|
|
127
|
+
.flush()
|
|
128
|
+
)
|
|
129
|
+
if not params:
|
|
130
|
+
return content
|
|
131
|
+
return rf"#par({render(RenderType.DICT)(params)})[{content}]"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def heading(
|
|
135
|
+
content: Block,
|
|
136
|
+
*,
|
|
137
|
+
level: int = 1,
|
|
138
|
+
supplement: Optional[Content] = None,
|
|
139
|
+
numbering: Optional[str] = None,
|
|
140
|
+
label: Optional[Label] = None,
|
|
141
|
+
) -> Block:
|
|
142
|
+
"""Interface of `heading` function in typst.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
content (Block): The heading's title.
|
|
146
|
+
level (int, optional): The absolute nesting depth of the heading, starting from one. Defaults to 1.
|
|
147
|
+
supplement (Optional[Content], optional): A supplement for the heading. Defaults to None.
|
|
148
|
+
numbering (Optional[str], optional): How to number the heading. Defaults to None.
|
|
149
|
+
label (Optional[Label], optional): Cross-reference for the heading. Defaults to None.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Block: Executable typst block.
|
|
153
|
+
|
|
154
|
+
Examples:
|
|
155
|
+
>>> heading("Hello, World!", level=2, supplement=Content("Chapter"), label=Label("chap:chapter"))
|
|
156
|
+
'#heading(supplement: [Chapter], level: 2)[Hello, World!] <chap:chapter>'
|
|
157
|
+
>>> heading("Hello, World!", level=2)
|
|
158
|
+
'== Hello, World!'
|
|
159
|
+
"""
|
|
160
|
+
params = (
|
|
161
|
+
Pipe({"supplement": supplement, "numbering": numbering})
|
|
162
|
+
.map(valfilter(lambda x: x is not None))
|
|
163
|
+
.flush()
|
|
164
|
+
)
|
|
165
|
+
if not params:
|
|
166
|
+
result = rf"{"="*level} {content}"
|
|
167
|
+
else:
|
|
168
|
+
result = rf"#heading({render(RenderType.DICT)(assoc(params,'level',level))})[{content}]"
|
|
169
|
+
if label:
|
|
170
|
+
result += f" {label}"
|
|
171
|
+
return result
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def image(
|
|
175
|
+
path: str,
|
|
176
|
+
*,
|
|
177
|
+
format: Optional[str] = None,
|
|
178
|
+
width: Optional[Relative] = None,
|
|
179
|
+
height: Optional[Relative] = None,
|
|
180
|
+
alt: Optional[str] = None,
|
|
181
|
+
fit: Optional[str] = None,
|
|
182
|
+
) -> Block:
|
|
183
|
+
"""Interface of `image` function in typst.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
path (str): Path to an image file.
|
|
187
|
+
format (Optional[str], optional): The image's format. Detected automatically by default. Options are "png", "jpg", "gif", and "svg". Defaults to None.
|
|
188
|
+
width (Optional[Relative], optional): The width of the image. Defaults to None.
|
|
189
|
+
height (Optional[Relative], optional): The height of the image. Defaults to None.
|
|
190
|
+
alt (Optional[str], optional): A text describing the image. Defaults to None.
|
|
191
|
+
fit (Optional[str], optional): How the image should adjust itself to a given area (the area is defined by the width and height fields). Note that fit doesn't visually change anything if the area's aspect ratio is the same as the image's one. Options are "cover", "contain", and "stretch". Defaults to None.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Block: Executable typst block.
|
|
195
|
+
|
|
196
|
+
Examples:
|
|
197
|
+
>>> image("image.png")
|
|
198
|
+
'#image("image.png")'
|
|
199
|
+
>>> image("image.png", format="png")
|
|
200
|
+
'#image("image.png", format: "png")'
|
|
201
|
+
"""
|
|
202
|
+
if format and format not in ("png", "jpg", "gif", "svg"):
|
|
203
|
+
raise ValueError(f"Invalid value for format: {format}.")
|
|
204
|
+
if fit and fit not in ("cover", "contain", "stretch"):
|
|
205
|
+
raise ValueError(f"Invalid value for fit: {fit}.")
|
|
206
|
+
params = (
|
|
207
|
+
Pipe(
|
|
208
|
+
{"format": format, "width": width, "height": height, "alt": alt, "fit": fit}
|
|
209
|
+
)
|
|
210
|
+
.map(valfilter(lambda x: x is not None))
|
|
211
|
+
.flush()
|
|
212
|
+
)
|
|
213
|
+
if not params:
|
|
214
|
+
return rf"#image({render(RenderType.VALUE)(path)})"
|
|
215
|
+
return (
|
|
216
|
+
rf"#image({render(RenderType.VALUE)(path)}, {render(RenderType.DICT)(params)})"
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _figure_caption(content: Block, *, separator: Optional[Content] = None) -> Content:
|
|
221
|
+
"""Interface of `figure.caption` function in typst.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
content (Block): The caption's body.
|
|
225
|
+
separator (Optional[Content], optional): The separator which will appear between the number and body. Defaults to None.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
Content: The caption's content.
|
|
229
|
+
"""
|
|
230
|
+
params = (
|
|
231
|
+
Pipe({"separator": separator}).map(valfilter(lambda x: x is not None)).flush()
|
|
232
|
+
)
|
|
233
|
+
if not params:
|
|
234
|
+
return Content(content)
|
|
235
|
+
return Content(rf"#figure.caption({render(RenderType.DICT)(params)})[{content}]")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@attach_func(_figure_caption, "caption")
|
|
239
|
+
def figure(
|
|
240
|
+
content: Block, *, caption: Optional[Content] = None, label: Optional[Label] = None
|
|
241
|
+
) -> Block:
|
|
242
|
+
"""Interface of `figure` function in typst.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
content (Block): The content of the figure. Often, an image.
|
|
246
|
+
caption (Optional[Content], optional): The figure's caption. Defaults to None.
|
|
247
|
+
label (Optional[Label], optional): Cross-reference for the figure. Defaults to None.
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
Block: Executable typst block.
|
|
251
|
+
|
|
252
|
+
Examples:
|
|
253
|
+
>>> figure(image("image.png"))
|
|
254
|
+
'#figure(image("image.png"))'
|
|
255
|
+
>>> figure(image("image.png"), caption=Content("This is a figure."))
|
|
256
|
+
'#figure(image("image.png"), caption: [This is a figure.])'
|
|
257
|
+
>>> figure(image("image.png"), caption=Content("This is a figure."), label=Label("fig:figure"))
|
|
258
|
+
'#figure(image("image.png"), caption: [This is a figure.]) <fig:figure>'
|
|
259
|
+
>>> figure(image("image.png"), caption=figure.caption("This is a figure.", separator=Content("---")))
|
|
260
|
+
'#figure(image("image.png"), caption: figure.caption(separator: [---])[This is a figure.])'
|
|
261
|
+
"""
|
|
262
|
+
params = Pipe({"caption": caption}).map(valfilter(lambda x: x is not None)).flush()
|
|
263
|
+
if not params:
|
|
264
|
+
result = rf"#figure({Content.examine_sharp(content)})"
|
|
265
|
+
else:
|
|
266
|
+
result = rf"#figure({Content.examine_sharp(content)}, {render(RenderType.DICT)(params)})"
|
|
267
|
+
if label:
|
|
268
|
+
result += f" {label}"
|
|
269
|
+
return result
|
typstpy/param_types.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Classes in this module should only be used as parameter types in the `functions` module."""
|
|
2
|
+
|
|
3
|
+
from itertools import starmap
|
|
4
|
+
from typing import TypeAlias, Union
|
|
5
|
+
|
|
6
|
+
from attrs import field, frozen
|
|
7
|
+
from cytoolz.curried import curry, map # type:ignore
|
|
8
|
+
from pymonad.reader import Pipe # type:ignore
|
|
9
|
+
|
|
10
|
+
from .utils import FormatType, format
|
|
11
|
+
|
|
12
|
+
Block: TypeAlias = str
|
|
13
|
+
"""Executable typst block."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@frozen
|
|
17
|
+
class Content:
|
|
18
|
+
content: Block = field()
|
|
19
|
+
|
|
20
|
+
@content.validator
|
|
21
|
+
def _check_content(self, attribute, value):
|
|
22
|
+
# todo: Check if the content is executable typst block.
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
def _can_simplify(self) -> bool:
|
|
26
|
+
return self.content.startswith("#")
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def examine_sharp(content: Block) -> str:
|
|
30
|
+
return content.lstrip("#")
|
|
31
|
+
|
|
32
|
+
def __str__(self) -> str:
|
|
33
|
+
if self._can_simplify():
|
|
34
|
+
return Content.examine_sharp(self.content)
|
|
35
|
+
return f"[{self.content}]"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@frozen
|
|
39
|
+
class Label:
|
|
40
|
+
label: str = field()
|
|
41
|
+
|
|
42
|
+
@label.validator
|
|
43
|
+
def _check_label(self, attribute, value):
|
|
44
|
+
# todo: Check for illegal characters in label.
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
def __str__(self) -> str:
|
|
48
|
+
return f"<{self.label}>"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@frozen
|
|
52
|
+
class Length:
|
|
53
|
+
value: float = field(repr=format(FormatType.FLOAT))
|
|
54
|
+
unit: str = field()
|
|
55
|
+
|
|
56
|
+
@unit.validator
|
|
57
|
+
def _check_unit(self, attribute, value):
|
|
58
|
+
if value not in ("pt", "mm", "cm", "em"):
|
|
59
|
+
raise ValueError(f"Invalid unit: {value}.")
|
|
60
|
+
|
|
61
|
+
def __pos__(self) -> "Length":
|
|
62
|
+
return self
|
|
63
|
+
|
|
64
|
+
def __neg__(self) -> "Length":
|
|
65
|
+
return Length(-self.value, self.unit)
|
|
66
|
+
|
|
67
|
+
def __add__(self, other: Union["Length", "Ratio", "_Relative"]) -> "_Relative":
|
|
68
|
+
if isinstance(other, (Length, Ratio)):
|
|
69
|
+
return _Relative((self, other), ("+", "+"))
|
|
70
|
+
elif isinstance(other, _Relative):
|
|
71
|
+
return _Relative((self,) + other.items, ("+",) + other.signs)
|
|
72
|
+
else:
|
|
73
|
+
raise TypeError(
|
|
74
|
+
f"Unsupported operand type(s) for +: 'Length' and '{type(other)}'."
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def __sub__(self, other: Union["Length", "Ratio", "_Relative"]) -> "_Relative":
|
|
78
|
+
if isinstance(other, (Length, Ratio)):
|
|
79
|
+
return _Relative((self, other), ("+", "-"))
|
|
80
|
+
elif isinstance(other, _Relative):
|
|
81
|
+
return _Relative((self,) + other.items, ("+",) + other.inverse_signs)
|
|
82
|
+
else:
|
|
83
|
+
raise TypeError(
|
|
84
|
+
f"Unsupported operand type(s) for -: 'Length' and '{type(other)}'."
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def __str__(self) -> str:
|
|
88
|
+
return f"{format(FormatType.FLOAT)(self.value)}{self.unit}"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@frozen
|
|
92
|
+
class Ratio:
|
|
93
|
+
value: float = field(repr=format(FormatType.FLOAT))
|
|
94
|
+
|
|
95
|
+
def __pos__(self) -> "Ratio":
|
|
96
|
+
return self
|
|
97
|
+
|
|
98
|
+
def __neg__(self) -> "Ratio":
|
|
99
|
+
return Ratio(-self.value)
|
|
100
|
+
|
|
101
|
+
def __add__(self, other: Union[Length, "Ratio", "_Relative"]) -> "_Relative":
|
|
102
|
+
if isinstance(other, (Length, Ratio)):
|
|
103
|
+
return _Relative((self, other), ("+", "+"))
|
|
104
|
+
elif isinstance(other, _Relative):
|
|
105
|
+
return _Relative((self,) + other.items, ("+",) + other.signs)
|
|
106
|
+
else:
|
|
107
|
+
raise TypeError(
|
|
108
|
+
f"Unsupported operand type(s) for +: 'Ratio' and '{type(other)}'."
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def __sub__(self, other: Union[Length, "Ratio", "_Relative"]) -> "_Relative":
|
|
112
|
+
if isinstance(other, (Length, Ratio)):
|
|
113
|
+
return _Relative((self, other), ("+", "-"))
|
|
114
|
+
elif isinstance(other, _Relative):
|
|
115
|
+
return _Relative((self,) + other.items, ("+",) + other.inverse_signs)
|
|
116
|
+
else:
|
|
117
|
+
raise TypeError(
|
|
118
|
+
f"Unsupported operand type(s) for -: 'Ratio' and '{type(other)}'."
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def __str__(self) -> str:
|
|
122
|
+
return f"{format(FormatType.FLOAT)(self.value)}%"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@frozen
|
|
126
|
+
class _Relative:
|
|
127
|
+
items: tuple[Length | Ratio, ...] = field()
|
|
128
|
+
signs: tuple[str, ...] = field()
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def inverse_signs(self) -> tuple[str, ...]:
|
|
132
|
+
def inverse(sign: str) -> str:
|
|
133
|
+
match sign:
|
|
134
|
+
case "+":
|
|
135
|
+
return "-"
|
|
136
|
+
case "-":
|
|
137
|
+
return "+"
|
|
138
|
+
case _:
|
|
139
|
+
raise ValueError(f"Invalid sign: {sign}.")
|
|
140
|
+
|
|
141
|
+
return Pipe(self.signs).map(map(inverse)).map(tuple).flush()
|
|
142
|
+
|
|
143
|
+
def __add__(self, other: Union[Length, Ratio, "_Relative"]) -> "_Relative":
|
|
144
|
+
if isinstance(other, (Length, Ratio)):
|
|
145
|
+
return _Relative(self.items + (other,), self.signs + ("+",))
|
|
146
|
+
elif isinstance(other, _Relative):
|
|
147
|
+
return _Relative(self.items + other.items, self.signs + other.signs)
|
|
148
|
+
else:
|
|
149
|
+
raise TypeError(
|
|
150
|
+
f"Unsupported operand type(s) for +: 'Relative' and '{type(other)}'."
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def __sub__(self, other: Union[Length, Ratio, "_Relative"]) -> "_Relative":
|
|
154
|
+
if isinstance(other, (Length, Ratio)):
|
|
155
|
+
return _Relative(self.items + (other,), self.signs + ("-",))
|
|
156
|
+
elif isinstance(other, _Relative):
|
|
157
|
+
return _Relative(self.items + other.items, self.signs + other.inverse_signs)
|
|
158
|
+
else:
|
|
159
|
+
raise TypeError(
|
|
160
|
+
f"Unsupported operand type(s) for -: 'Relative' and '{type(other)}'."
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def __str__(self) -> str:
|
|
164
|
+
return (
|
|
165
|
+
Pipe(zip(self.signs, self.items))
|
|
166
|
+
.map(curry(starmap)(lambda x, y: f"{x}{y}"))
|
|
167
|
+
.map(lambda x: ("".join(x)).lstrip("+"))
|
|
168
|
+
.map(
|
|
169
|
+
lambda x: x.replace("+-", "-")
|
|
170
|
+
.replace("--", "+")
|
|
171
|
+
.replace("-+", "-")
|
|
172
|
+
.replace("++", "+")
|
|
173
|
+
)
|
|
174
|
+
.flush()
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
Relative: TypeAlias = Length | Ratio | _Relative
|
typstpy/utils.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from enum import Enum, auto
|
|
2
|
+
from typing import Any, Callable, Optional
|
|
3
|
+
|
|
4
|
+
from cytoolz.curried import curry, map # type:ignore
|
|
5
|
+
|
|
6
|
+
# region render
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RenderType(Enum):
|
|
10
|
+
KEY = auto()
|
|
11
|
+
VALUE = auto()
|
|
12
|
+
DICT = auto()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _render(render_type: RenderType) -> Callable[[Any], str]:
|
|
16
|
+
def render_key(key: str) -> str:
|
|
17
|
+
return key.replace("_", "-")
|
|
18
|
+
|
|
19
|
+
def render_value(value: Any) -> str:
|
|
20
|
+
match value:
|
|
21
|
+
case bool():
|
|
22
|
+
return "true" if value else "false"
|
|
23
|
+
case str():
|
|
24
|
+
return f'"{value}"'
|
|
25
|
+
case tuple():
|
|
26
|
+
return f"({', '.join(map(render_value, value))})"
|
|
27
|
+
case _:
|
|
28
|
+
return str(value)
|
|
29
|
+
|
|
30
|
+
def render_dict(params: dict[str, Any]) -> str:
|
|
31
|
+
if not params:
|
|
32
|
+
return ""
|
|
33
|
+
return ", ".join(
|
|
34
|
+
f"{render_key(k)}: {render_value(v)}" for k, v in params.items()
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
match render_type:
|
|
38
|
+
case RenderType.KEY:
|
|
39
|
+
return render_key
|
|
40
|
+
case RenderType.VALUE:
|
|
41
|
+
return render_value
|
|
42
|
+
case RenderType.DICT:
|
|
43
|
+
return render_dict
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@curry
|
|
47
|
+
def render(render_type: RenderType, target: Any) -> str:
|
|
48
|
+
return _render(render_type)(target)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# endregion
|
|
52
|
+
# region format
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class FormatType(Enum):
|
|
56
|
+
FLOAT = auto()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _format(format_type: FormatType) -> Callable[[Any], str]:
|
|
60
|
+
def format_float(value: float) -> str:
|
|
61
|
+
return f"{value:.2f}".rstrip("0").rstrip(".")
|
|
62
|
+
|
|
63
|
+
match format_type:
|
|
64
|
+
case FormatType.FLOAT:
|
|
65
|
+
return format_float
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@curry
|
|
69
|
+
def format(format_type: FormatType, target: Any) -> str:
|
|
70
|
+
return _format(format_type)(target)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# endregion
|
|
74
|
+
# region decorator
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def attach_func(func: Callable, name: Optional[str] = None) -> Callable:
|
|
78
|
+
def wrapper(_func: Callable) -> Callable:
|
|
79
|
+
_name = name if name else _func.__name__
|
|
80
|
+
if _name.startswith("_"):
|
|
81
|
+
raise ValueError(f"Invalid name: {_name}.")
|
|
82
|
+
setattr(_func, _name, func)
|
|
83
|
+
return _func
|
|
84
|
+
|
|
85
|
+
return wrapper
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# endregion
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 chenjunhan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: typstpy
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Python interface to generate Typst code.
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: chenjunhan
|
|
7
|
+
Author-email: beibingyangliuying@foxmail.com
|
|
8
|
+
Requires-Python: >=3.12,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Requires-Dist: attrs (>=24.2.0,<25.0.0)
|
|
13
|
+
Requires-Dist: cytoolz (>=0.12.3,<0.13.0)
|
|
14
|
+
Requires-Dist: pandas (>=2.2.3,<3.0.0)
|
|
15
|
+
Requires-Dist: pymonad (>=2.4.0,<3.0.0)
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# python-typst
|
|
19
|
+
|
|
20
|
+
`python-typst` is a library for generating executable typst code (See [typst repository](https://github.com/typst/typst) and [typst documentation](https://typst.app/docs/) for more information).
|
|
21
|
+
It is written primarily in functional programming paradigm with some OOP content.
|
|
22
|
+
|
|
23
|
+
This package provides the interfaces in a way that is as close as possible to typst's native functions.
|
|
24
|
+
Through `python-typst` and other data processing packages, you can generate data reports quickly.
|
|
25
|
+
|
|
26
|
+
Contributions are welcome.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
## Examples
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
>>> from typstpy import emph, figure, heading, image, par, strong, text, Block, Content, Label, Length, Ratio, Relative
|
|
34
|
+
>>> text("Hello, World!", font="Arial", fallback=True)
|
|
35
|
+
'#text(font: "Arial", fallback: true)[Hello, World!]'
|
|
36
|
+
>>> text("Hello, World!", font=("Arial", "Times New Roman"), fallback=True)
|
|
37
|
+
'#text(font: ("Arial", "Times New Roman"), fallback: true)[Hello, World!]'
|
|
38
|
+
>>> text("Hello, World!", size=Length(12, "pt"))
|
|
39
|
+
'#text(size: 12pt)[Hello, World!]'
|
|
40
|
+
>>> emph("Hello, World!")
|
|
41
|
+
'#emph[Hello, World!]'
|
|
42
|
+
>>> strong("Hello, World!")
|
|
43
|
+
'#strong[Hello, World!]'
|
|
44
|
+
>>> strong("Hello, World!", delta=300)
|
|
45
|
+
'#strong(delta: 300)[Hello, World!]'
|
|
46
|
+
>>> par("Hello, World!", leading=Length(1.5, "em"))
|
|
47
|
+
'#par(leading: 1.5em)[Hello, World!]'
|
|
48
|
+
>>> par("Hello, World!", justify=True)
|
|
49
|
+
'#par(justify: true)[Hello, World!]'
|
|
50
|
+
>>> par("Hello, World!")
|
|
51
|
+
'Hello, World!'
|
|
52
|
+
>>> heading("Hello, World!", level=2, supplement=Content("Chapter"), label=Label("chap:chapter"))
|
|
53
|
+
'#heading(supplement: [Chapter], level: 2)[Hello, World!] <chap:chapter>'
|
|
54
|
+
>>> heading("Hello, World!", level=2)
|
|
55
|
+
'== Hello, World!'
|
|
56
|
+
>>> image("image.png")
|
|
57
|
+
'#image("image.png")'
|
|
58
|
+
>>> image("image.png", format="png")
|
|
59
|
+
'#image("image.png", format: "png")'
|
|
60
|
+
>>> figure(image("image.png"))
|
|
61
|
+
'#figure(image("image.png"))'
|
|
62
|
+
>>> figure(image("image.png"), caption=Content("This is a figure."))
|
|
63
|
+
'#figure(image("image.png"), caption: [This is a figure.])'
|
|
64
|
+
>>> figure(image("image.png"), caption=Content("This is a figure."), label=Label("fig:figure"))
|
|
65
|
+
'#figure(image("image.png"), caption: [This is a figure.]) <fig:figure>'
|
|
66
|
+
>>> figure(image("image.png"), caption=figure.caption("This is a figure.", separator=Content("---")))
|
|
67
|
+
'#figure(image("image.png"), caption: figure.caption(separator: [---])[This is a figure.])'
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## ChangeLog
|
|
71
|
+
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
typstpy/__init__.py,sha256=VPpR_5JxwsXXlhAHNizkF6Upxotk7L6kB4zdnOo0TWc,351
|
|
2
|
+
typstpy/functions.py,sha256=Hmil81Dex7T0wlWGRmaxVjnqyxrh4ic1Qmhfv3kI-A4,10439
|
|
3
|
+
typstpy/param_types.py,sha256=a16Fb3DiYJvqI_h1pnBWb24cPBckr38VEcg8xWCUsXo,5879
|
|
4
|
+
typstpy/utils.py,sha256=dLYTZFpOIqMVwZ22lamNcMAtifMpd18vhfZblPGigh4,2132
|
|
5
|
+
typstpy-0.0.1.dist-info/LICENSE.txt,sha256=fNIn9UAyIuqgCgEHHGPcmDoRAZFt5wLULxMvZCv2-rk,1086
|
|
6
|
+
typstpy-0.0.1.dist-info/METADATA,sha256=Mwzi3yL1fMMbRBJzdoj2KIdTCwZ4ttMDZwkjexLI-sI,2893
|
|
7
|
+
typstpy-0.0.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
8
|
+
typstpy-0.0.1.dist-info/RECORD,,
|