1
|
|
|
#!/usr/bin/env python |
2
|
|
|
|
3
|
|
|
""" |
4
|
|
|
Pandoc filter for adding tip in LaTeX. |
5
|
|
|
""" |
6
|
|
|
from __future__ import annotations |
7
|
|
|
|
8
|
|
|
import operator |
9
|
|
|
import os |
10
|
|
|
import pathlib |
11
|
|
|
import re |
12
|
|
|
import sys |
13
|
|
|
import tempfile |
14
|
|
|
from typing import Any |
15
|
|
|
|
16
|
|
|
import PIL.Image |
17
|
|
|
import PIL.ImageColor |
18
|
|
|
import PIL.ImageDraw |
19
|
|
|
import PIL.ImageFont |
20
|
|
|
|
21
|
|
|
from cleo.application import Application |
22
|
|
|
from cleo.commands.command import Command |
23
|
|
|
|
24
|
|
|
import fontTools.ttLib |
25
|
|
|
|
26
|
|
|
from panflute import ( |
27
|
|
|
BulletList, |
28
|
|
|
Code, |
29
|
|
|
CodeBlock, |
30
|
|
|
DefinitionList, |
31
|
|
|
Div, |
32
|
|
|
Doc, |
33
|
|
|
Element, |
34
|
|
|
Figure, |
35
|
|
|
HorizontalRule, |
36
|
|
|
Image, |
37
|
|
|
LineBlock, |
38
|
|
|
Link, |
39
|
|
|
MetaInlines, |
40
|
|
|
MetaList, |
41
|
|
|
OrderedList, |
42
|
|
|
Para, |
43
|
|
|
Plain, |
44
|
|
|
RawBlock, |
45
|
|
|
RawInline, |
46
|
|
|
Span, |
47
|
|
|
convert_text, |
48
|
|
|
debug, |
49
|
|
|
run_filter, |
50
|
|
|
) |
51
|
|
|
|
52
|
|
|
import platformdirs |
53
|
|
|
|
54
|
|
|
import tinycss2 |
55
|
|
|
|
56
|
|
|
import yaml |
57
|
|
|
|
58
|
|
|
|
59
|
|
|
class IconFont: |
60
|
|
|
""" |
61
|
|
|
Base class that represents web icon font. |
62
|
|
|
|
63
|
|
|
This class has been gratly inspired by the code found |
64
|
|
|
in https://github.com/Pythonity/icon-font-to-png |
65
|
|
|
|
66
|
|
|
Arguments |
67
|
|
|
--------- |
68
|
|
|
css_file |
69
|
|
|
path to icon font CSS file |
70
|
|
|
ttf_file |
71
|
|
|
path to icon font TTF file |
72
|
|
|
prefix |
73
|
|
|
new prefix if any |
74
|
|
|
""" |
75
|
|
|
|
76
|
|
|
def __init__( |
77
|
|
|
self, |
78
|
|
|
css_file: pathlib.Path, |
79
|
|
|
ttf_file: pathlib.Path, |
80
|
|
|
prefix: str | None = None, |
81
|
|
|
) -> None: |
82
|
|
|
self.css_file = css_file |
83
|
|
|
self.ttf_file = ttf_file |
84
|
|
|
self.css_icons = self.load_css(prefix) |
85
|
|
|
|
86
|
|
|
def load_css(self, prefix: str | None) -> dict[str, str]: |
87
|
|
|
""" |
88
|
|
|
Create a dict of all icons available in CSS file. |
89
|
|
|
|
90
|
|
|
Arguments |
91
|
|
|
--------- |
92
|
|
|
prefix |
93
|
|
|
new prefix if any |
94
|
|
|
|
95
|
|
|
Returns |
96
|
|
|
------- |
97
|
|
|
dict[str, str] |
98
|
|
|
sorted icons dict |
99
|
|
|
""" |
100
|
|
|
# pylint: disable=too-many-locals |
101
|
|
|
icons = {} |
102
|
|
|
common = None |
103
|
|
|
with self.css_file.open() as stream: |
104
|
|
|
rules = tinycss2.parse_stylesheet(stream.read()) |
105
|
|
|
font = fontTools.ttLib.TTFont(self.ttf_file) |
106
|
|
|
prelude_regex = re.compile("\\.([^:]*):?:before,?") |
107
|
|
|
content_regex = re.compile("\\s*content:\\s*([^;]+);") |
108
|
|
|
|
109
|
|
|
# pylint: disable=too-many-nested-blocks |
110
|
|
|
for rule in rules: |
111
|
|
|
if rule.type == "qualified-rule": |
112
|
|
|
prelude = tinycss2.serialize(rule.prelude) |
113
|
|
|
content = tinycss2.serialize(rule.content) |
114
|
|
|
prelude_result = prelude_regex.match(prelude) |
115
|
|
|
content_result = content_regex.match(content) |
116
|
|
|
if prelude_result and content_result: |
117
|
|
|
name = prelude_result.group(1) |
118
|
|
|
character = content_result.group(1)[1:-1] |
119
|
|
|
for cmap in font["cmap"].tables: |
120
|
|
|
if cmap.isUnicode() and ord(character) in cmap.cmap: |
121
|
|
|
if common is None: |
122
|
|
|
common = name |
123
|
|
|
else: |
124
|
|
|
common = os.path.commonprefix((common, name)) |
125
|
|
|
icons[name] = character |
126
|
|
|
break |
127
|
|
|
|
128
|
|
|
common = common or "" |
129
|
|
|
|
130
|
|
|
# Remove common prefix |
131
|
|
|
if prefix: |
132
|
|
|
icons = { |
133
|
|
|
prefix + name[len(common) :]: value for name, value in icons.items() |
134
|
|
|
} |
135
|
|
|
|
136
|
|
|
return dict(sorted(icons.items(), key=operator.itemgetter(0))) |
137
|
|
|
|
138
|
|
|
# pylint: disable=too-many-arguments,too-many-locals |
139
|
|
|
def export_icon( |
140
|
|
|
self, |
141
|
|
|
icon: str, |
142
|
|
|
size: int, |
143
|
|
|
color: str = "black", |
144
|
|
|
scale: float | str = "auto", |
145
|
|
|
filename: str | None = None, |
146
|
|
|
export_dir: str = "exported", |
147
|
|
|
) -> None: |
148
|
|
|
""" |
149
|
|
|
Export given icon with provided parameters. |
150
|
|
|
|
151
|
|
|
If the desired icon size is less than 150x150 pixels, we will first |
152
|
|
|
create a 150x150 pixels image and then scale it down, so that |
153
|
|
|
it's much less likely that the edges of the icon end up cropped. |
154
|
|
|
|
155
|
|
|
Parameters |
156
|
|
|
---------- |
157
|
|
|
icon |
158
|
|
|
valid icon name |
159
|
|
|
size |
160
|
|
|
icon size in pixels |
161
|
|
|
color |
162
|
|
|
color name or hex value |
163
|
|
|
scale |
164
|
|
|
scaling factor between 0 and 1, or 'auto' for automatic scaling |
165
|
|
|
filename |
166
|
|
|
name of the output file |
167
|
|
|
export_dir |
168
|
|
|
path to export directory |
169
|
|
|
""" |
170
|
|
|
org_size = size |
171
|
|
|
size = max(150, size) |
172
|
|
|
|
173
|
|
|
image = PIL.Image.new("RGBA", (size, size), color=(0, 0, 0, 0)) |
174
|
|
|
draw = PIL.ImageDraw.Draw(image) |
175
|
|
|
|
176
|
|
|
if scale == "auto": |
177
|
|
|
scale_factor = 1.0 |
178
|
|
|
else: |
179
|
|
|
scale_factor = float(scale) |
180
|
|
|
|
181
|
|
|
font_size = int(size * scale_factor) |
182
|
|
|
font = PIL.ImageFont.truetype(self.ttf_file, font_size) |
183
|
|
|
width = draw.textlength(self.css_icons[icon], font=font) |
184
|
|
|
height = font_size # always, as long as single-line of text |
185
|
|
|
|
186
|
|
|
# If auto-scaling is enabled, we need to make sure the resulting |
187
|
|
|
# graphic fits inside the boundary. The values are rounded and may be |
188
|
|
|
# off by a pixel or two, so we may need to do a few iterations. |
189
|
|
|
# The use of a decrementing multiplication factor protects us from |
190
|
|
|
# getting into an infinite loop. |
191
|
|
|
if scale == "auto": |
192
|
|
|
iteration = 0 |
193
|
|
|
factor = 1.0 |
194
|
|
|
|
195
|
|
|
while True: |
196
|
|
|
width = draw.textlength(self.css_icons[icon], font=font) |
197
|
|
|
|
198
|
|
|
# Check if the image fits |
199
|
|
|
dim = max(width, height) |
200
|
|
|
if dim > size: |
201
|
|
|
font = PIL.ImageFont.truetype( |
202
|
|
|
self.ttf_file, |
203
|
|
|
int(size * size / dim * factor), |
204
|
|
|
) |
205
|
|
|
else: |
206
|
|
|
break |
207
|
|
|
|
208
|
|
|
# Adjust the factor every two iterations |
209
|
|
|
iteration += 1 |
210
|
|
|
if iteration % 2 == 0: |
211
|
|
|
factor *= 0.99 |
212
|
|
|
|
213
|
|
|
draw.text( |
214
|
|
|
((size - width) / 2, (size - height) / 2), |
215
|
|
|
self.css_icons[icon], |
216
|
|
|
font=font, |
217
|
|
|
fill=color, |
218
|
|
|
) |
219
|
|
|
|
220
|
|
|
# Get bounding box |
221
|
|
|
bbox = image.getbbox() |
222
|
|
|
|
223
|
|
|
# Create an alpha mask |
224
|
|
|
image_mask = PIL.Image.new("L", (size, size)) |
225
|
|
|
draw_mask = PIL.ImageDraw.Draw(image_mask) |
226
|
|
|
|
227
|
|
|
# Draw the icon on the mask |
228
|
|
|
draw_mask.text( |
229
|
|
|
((size - width) / 2, (size - height) / 2), |
230
|
|
|
self.css_icons[icon], |
231
|
|
|
font=font, |
232
|
|
|
fill=255, |
233
|
|
|
) |
234
|
|
|
|
235
|
|
|
# Create a solid color image and apply the mask |
236
|
|
|
icon_image = PIL.Image.new("RGBA", (size, size), color) |
237
|
|
|
icon_image.putalpha(image_mask) |
238
|
|
|
|
239
|
|
|
if bbox: |
240
|
|
|
icon_image = icon_image.crop(bbox) |
241
|
|
|
|
242
|
|
|
border_w = int((size - (bbox[2] - bbox[0])) / 2) # type: ignore |
243
|
|
|
border_h = int((size - (bbox[3] - bbox[1])) / 2) # type: ignore |
244
|
|
|
|
245
|
|
|
# Create output image |
246
|
|
|
out_image = PIL.Image.new("RGBA", (size, size), (0, 0, 0, 0)) |
247
|
|
|
out_image.paste(icon_image, (border_w, border_h)) |
248
|
|
|
|
249
|
|
|
# If necessary, scale the image to the target size |
250
|
|
|
if org_size != size: |
251
|
|
|
out_image = out_image.resize( |
252
|
|
|
(org_size, org_size), |
253
|
|
|
PIL.Image.Resampling.LANCZOS, |
254
|
|
|
) |
255
|
|
|
|
256
|
|
|
# Make sure export directory exists |
257
|
|
|
if not pathlib.Path(export_dir).exists(): |
258
|
|
|
pathlib.Path(export_dir).mkdir(parents=True) |
259
|
|
|
|
260
|
|
|
# Default filename |
261
|
|
|
if not filename: |
262
|
|
|
filename = icon + ".png" |
263
|
|
|
|
264
|
|
|
# Save file |
265
|
|
|
out_image.save(os.path.join(export_dir, filename)) |
266
|
|
|
|
267
|
|
|
|
268
|
|
|
def load_icons() -> dict[str, IconFont]: |
269
|
|
|
""" |
270
|
|
|
Get the icons. |
271
|
|
|
|
272
|
|
|
Returns |
273
|
|
|
------- |
274
|
|
|
dict["str", IconFont] |
275
|
|
|
A dictionnary from icon name to IconFont. |
276
|
|
|
""" |
277
|
|
|
icons = {} |
278
|
|
|
icon_font = IconFont( |
279
|
|
|
css_file=pathlib.Path( |
280
|
|
|
sys.prefix, "share", "pandoc_latex_tip", "fontawesome", "fontawesome.css" |
281
|
|
|
), |
282
|
|
|
ttf_file=pathlib.Path( |
283
|
|
|
sys.prefix, "share", "pandoc_latex_tip", "fontawesome", "fa-solid-900.ttf" |
284
|
|
|
), |
285
|
|
|
) |
286
|
|
|
icons.update({key: icon_font for key in icon_font.css_icons}) |
287
|
|
|
icon_font = IconFont( |
288
|
|
|
css_file=pathlib.Path( |
289
|
|
|
sys.prefix, "share", "pandoc_latex_tip", "fontawesome", "fontawesome.css" |
290
|
|
|
), |
291
|
|
|
ttf_file=pathlib.Path( |
292
|
|
|
sys.prefix, "share", "pandoc_latex_tip", "fontawesome", "fa-regular-400.ttf" |
293
|
|
|
), |
294
|
|
|
prefix="far-", |
295
|
|
|
) |
296
|
|
|
icons.update({key: icon_font for key in icon_font.css_icons}) |
297
|
|
|
icon_font = IconFont( |
298
|
|
|
css_file=pathlib.Path( |
299
|
|
|
sys.prefix, "share", "pandoc_latex_tip", "fontawesome", "brands.css" |
300
|
|
|
), |
301
|
|
|
ttf_file=pathlib.Path( |
302
|
|
|
sys.prefix, "share", "pandoc_latex_tip", "fontawesome", "fa-brands-400.ttf" |
303
|
|
|
), |
304
|
|
|
) |
305
|
|
|
icons.update({key: icon_font for key in icon_font.css_icons}) |
306
|
|
|
|
307
|
|
|
config_path = pathlib.Path(sys.prefix, "share", "pandoc_latex_tip", "config.yml") |
308
|
|
|
if config_path.exists(): |
309
|
|
|
with config_path.open(encoding="utf-8") as stream: |
310
|
|
|
config = yaml.safe_load(stream) |
311
|
|
|
for collection, definition in config.items(): |
312
|
|
|
if "css" not in definition: |
313
|
|
|
break |
314
|
|
|
if "css" not in definition: |
315
|
|
|
break |
316
|
|
|
css_file = definition["css"] |
317
|
|
|
if "ttf" not in definition: |
318
|
|
|
break |
319
|
|
|
ttf_file = definition["ttf"] |
320
|
|
|
icon_font = IconFont( |
321
|
|
|
css_file=pathlib.Path( |
322
|
|
|
sys.prefix, |
323
|
|
|
"share", |
324
|
|
|
"pandoc_latex_tip", |
325
|
|
|
collection, |
326
|
|
|
css_file, |
327
|
|
|
), |
328
|
|
|
ttf_file=pathlib.Path( |
329
|
|
|
sys.prefix, |
330
|
|
|
"share", |
331
|
|
|
"pandoc_latex_tip", |
332
|
|
|
collection, |
333
|
|
|
ttf_file, |
334
|
|
|
), |
335
|
|
|
prefix=definition.get("prefix", False), |
336
|
|
|
) |
337
|
|
|
icons.update({key: icon_font for key in icon_font.css_icons}) |
338
|
|
|
|
339
|
|
|
return icons |
340
|
|
|
|
341
|
|
|
|
342
|
|
|
def tip(elem: Element, doc: Doc) -> list[Element] | None: |
343
|
|
|
""" |
344
|
|
|
Apply tip transformation to element. |
345
|
|
|
|
346
|
|
|
Parameters |
347
|
|
|
---------- |
348
|
|
|
elem |
349
|
|
|
The element |
350
|
|
|
doc |
351
|
|
|
The original document. |
352
|
|
|
|
353
|
|
|
Returns |
354
|
|
|
------- |
355
|
|
|
list[Element] | None |
356
|
|
|
The additional elements if any. |
357
|
|
|
""" |
358
|
|
|
# Is it in the right format and is it a Span, Div? |
359
|
|
|
if doc.format in ("latex", "beamer") and isinstance( |
360
|
|
|
elem, (Span, Div, Code, CodeBlock) |
361
|
|
|
): |
362
|
|
|
# Is there a latex-tip-icon attribute? |
363
|
|
|
if "latex-tip-icon" in elem.attributes: |
364
|
|
|
return add_latex( |
365
|
|
|
elem, |
366
|
|
|
latex_code( |
367
|
|
|
doc, |
368
|
|
|
elem.attributes, |
369
|
|
|
{ |
370
|
|
|
"icon": "latex-tip-icon", |
371
|
|
|
"position": "latex-tip-position", |
372
|
|
|
"size": "latex-tip-size", |
373
|
|
|
"color": "latex-tip-color", |
374
|
|
|
"link": "latex-tip-link", |
375
|
|
|
}, |
376
|
|
|
), |
377
|
|
|
) |
378
|
|
|
|
379
|
|
|
# Get the classes |
380
|
|
|
classes = set(elem.classes) |
381
|
|
|
|
382
|
|
|
# Loop on all font size definition |
383
|
|
|
# noinspection PyUnresolvedReferences |
384
|
|
|
for definition in doc.defined: |
385
|
|
|
# Are the classes correct? |
386
|
|
|
if classes >= definition["classes"]: |
387
|
|
|
return add_latex(elem, definition["latex"]) |
388
|
|
|
|
389
|
|
|
return None |
390
|
|
|
|
391
|
|
|
|
392
|
|
|
def add_latex(elem: Element, latex: str) -> list[Element] | None: |
393
|
|
|
""" |
394
|
|
|
Add latex code. |
395
|
|
|
|
396
|
|
|
Parameters |
397
|
|
|
---------- |
398
|
|
|
elem |
399
|
|
|
Current element |
400
|
|
|
latex |
401
|
|
|
Latex code |
402
|
|
|
|
403
|
|
|
Returns |
404
|
|
|
------- |
405
|
|
|
list[Element] | None |
406
|
|
|
The additional elements if any. |
407
|
|
|
""" |
408
|
|
|
# pylint: disable=too-many-return-statements |
409
|
|
|
if bool(latex): |
410
|
|
|
# Is it a Span or a Code? |
411
|
|
|
if isinstance(elem, (Span, Code)): |
412
|
|
|
return [elem, RawInline(latex, "tex")] |
413
|
|
|
|
414
|
|
|
# It is a CodeBlock: create a minipage to ensure the |
415
|
|
|
# _tip to be on the same page as the codeblock |
416
|
|
|
if isinstance(elem, CodeBlock): |
417
|
|
|
return [ |
418
|
|
|
RawBlock(f"\\begin{{minipage}}{{\\textwidth}}{latex}", "tex"), |
419
|
|
|
elem, |
420
|
|
|
RawBlock("\\end{minipage}", "tex"), |
421
|
|
|
] |
422
|
|
|
|
423
|
|
|
while elem.content and isinstance(elem.content[0], Div): |
424
|
|
|
elem = elem.content[0] |
425
|
|
|
|
426
|
|
|
if not elem.content or isinstance( |
427
|
|
|
elem.content[0], (HorizontalRule, Figure, RawBlock, DefinitionList) |
428
|
|
|
): |
429
|
|
|
elem.content.insert(0, RawBlock(latex, "tex")) |
430
|
|
|
return None |
431
|
|
|
if isinstance(elem.content[0], (Plain, Para)): |
432
|
|
|
elem.content[0].content.insert(1, RawInline(latex, "tex")) |
433
|
|
|
return None |
434
|
|
|
if isinstance(elem.content[0], LineBlock): |
435
|
|
|
elem.content[0].content[0].content.insert(1, RawInline(latex, "tex")) |
436
|
|
|
return None |
437
|
|
|
if isinstance(elem.content[0], CodeBlock): |
438
|
|
|
elem.content.insert( |
439
|
|
|
0, |
440
|
|
|
RawBlock(f"\\begin{{minipage}}{{\\textwidth}}{latex}", "tex"), |
441
|
|
|
) |
442
|
|
|
elem.content.insert(2, RawBlock("\\end{minipage}", "tex")) |
443
|
|
|
return None |
444
|
|
|
if isinstance(elem.content[0], (BulletList, OrderedList)): |
445
|
|
|
elem.content[0].content[0].content[0].content.insert( # noqa: ECE001 |
446
|
|
|
1, |
447
|
|
|
RawInline(latex, "tex"), |
448
|
|
|
) |
449
|
|
|
return None |
450
|
|
|
debug("[WARNING] pandoc-latex-tip: Bad usage") |
451
|
|
|
return None |
452
|
|
|
|
453
|
|
|
|
454
|
|
|
# pylint: disable=too-many-arguments,too-many-locals |
455
|
|
|
def latex_code(doc: Doc, definition: dict[str, Any], keys: dict[str, str]) -> str: |
456
|
|
|
""" |
457
|
|
|
Get the latex code. |
458
|
|
|
|
459
|
|
|
Parameters |
460
|
|
|
---------- |
461
|
|
|
doc |
462
|
|
|
The original document |
463
|
|
|
definition |
464
|
|
|
The defition |
465
|
|
|
keys |
466
|
|
|
Key mapping |
467
|
|
|
|
468
|
|
|
Returns |
469
|
|
|
------- |
470
|
|
|
str |
471
|
|
|
The latex code. |
472
|
|
|
""" |
473
|
|
|
# Get the default color |
474
|
|
|
color = str(definition.get(keys["color"], "black")) |
475
|
|
|
|
476
|
|
|
# Get the size |
477
|
|
|
size = get_size(str(definition.get(keys["size"], "18"))) |
478
|
|
|
|
479
|
|
|
# Get the prefixes |
480
|
|
|
# noinspection PyArgumentEqualDefault |
481
|
|
|
prefix_odd = get_prefix(str(definition.get(keys["position"], "")), odd=True) |
482
|
|
|
prefix_even = get_prefix(str(definition.get(keys["position"], "")), odd=False) |
483
|
|
|
|
484
|
|
|
# Get the link |
485
|
|
|
link = str(definition.get(keys["link"], "")) |
486
|
|
|
|
487
|
|
|
# Get the icons |
488
|
|
|
icons = get_icons(doc, definition, keys["icon"], color, link) |
489
|
|
|
|
490
|
|
|
# Get the images |
491
|
|
|
images = create_images(doc, icons, size) |
492
|
|
|
|
493
|
|
|
if bool(images): |
494
|
|
|
# pylint: disable=consider-using-f-string |
495
|
|
|
return f""" |
496
|
|
|
\\checkoddpage%% |
497
|
|
|
\\ifoddpage%% |
498
|
|
|
{prefix_odd}%% |
499
|
|
|
\\else%% |
500
|
|
|
{prefix_even}%% |
501
|
|
|
\\fi%% |
502
|
|
|
\\marginnote{{{''.join(images)}}}[0pt]\\vspace{{0cm}}%% |
503
|
|
|
""" |
504
|
|
|
|
505
|
|
|
return "" |
506
|
|
|
|
507
|
|
|
|
508
|
|
|
def get_icons( |
509
|
|
|
doc: Doc, |
510
|
|
|
definition: dict[str, Any], |
511
|
|
|
key_icons: str, |
512
|
|
|
color: str, |
513
|
|
|
link: str, |
514
|
|
|
) -> list[dict[str, Any]]: |
515
|
|
|
""" |
516
|
|
|
Get tge icons. |
517
|
|
|
|
518
|
|
|
Parameters |
519
|
|
|
---------- |
520
|
|
|
doc |
521
|
|
|
The original document |
522
|
|
|
definition |
523
|
|
|
The definition |
524
|
|
|
key_icons |
525
|
|
|
A key mapping |
526
|
|
|
color |
527
|
|
|
The color |
528
|
|
|
link |
529
|
|
|
The link |
530
|
|
|
|
531
|
|
|
Returns |
532
|
|
|
------- |
533
|
|
|
list[dict[str, Any]] |
534
|
|
|
A list of icon definitions. |
535
|
|
|
""" |
536
|
|
|
# Test the icons definition |
537
|
|
|
if key_icons in definition: |
538
|
|
|
icons: list[dict[str, str]] = [] |
539
|
|
|
# pylint: disable=invalid-name |
540
|
|
|
if isinstance(definition[key_icons], list): |
541
|
|
|
for icon in definition[key_icons]: |
542
|
|
|
try: |
543
|
|
|
icon["color"] = icon.get("color", color) |
544
|
|
|
icon["link"] = icon.get("link", link) |
545
|
|
|
except AttributeError: |
546
|
|
|
icon = { |
547
|
|
|
"name": icon, |
548
|
|
|
"color": color, |
549
|
|
|
"link": link, |
550
|
|
|
} |
551
|
|
|
|
552
|
|
|
add_icon(doc, icons, icon) |
553
|
|
|
else: |
554
|
|
|
# noinspection PyUnresolvedReferences |
555
|
|
|
if definition[key_icons] in doc.icons: |
556
|
|
|
icons = [ |
557
|
|
|
{ |
558
|
|
|
"name": definition[key_icons], |
559
|
|
|
"color": color, |
560
|
|
|
"link": link, |
561
|
|
|
} |
562
|
|
|
] |
563
|
|
|
else: |
564
|
|
|
icons = [] |
565
|
|
|
else: |
566
|
|
|
icons = [ |
567
|
|
|
{ |
568
|
|
|
"name": "fa-exclamation-circle", |
569
|
|
|
"color": color, |
570
|
|
|
"link": link, |
571
|
|
|
} |
572
|
|
|
] |
573
|
|
|
|
574
|
|
|
return icons |
575
|
|
|
|
576
|
|
|
|
577
|
|
|
def add_icon(doc: Doc, icons: list[dict[str, str]], icon: dict[str, str]) -> None: |
578
|
|
|
""" |
579
|
|
|
Add icon. |
580
|
|
|
|
581
|
|
|
Parameters |
582
|
|
|
---------- |
583
|
|
|
doc |
584
|
|
|
The original document. |
585
|
|
|
icons |
586
|
|
|
A list of icon definition |
587
|
|
|
icon |
588
|
|
|
A potential new icon |
589
|
|
|
""" |
590
|
|
|
if "name" not in icon: |
591
|
|
|
# Bad formed icon |
592
|
|
|
debug("[WARNING] pandoc-latex-tip: Bad formed icon") |
593
|
|
|
return |
594
|
|
|
|
595
|
|
|
# Lower the color |
596
|
|
|
lower_color = icon["color"].lower() |
597
|
|
|
|
598
|
|
|
# Convert the color to black if unexisting |
599
|
|
|
# pylint: disable=import-outside-toplevel |
600
|
|
|
|
601
|
|
|
if lower_color not in PIL.ImageColor.colormap: |
602
|
|
|
debug( |
603
|
|
|
f"[WARNING] pandoc-latex-tip: {lower_color}" |
604
|
|
|
" is not a correct color name; using black" |
605
|
|
|
) |
606
|
|
|
lower_color = "black" |
607
|
|
|
|
608
|
|
|
# Is the icon correct? |
609
|
|
|
try: |
610
|
|
|
# noinspection PyUnresolvedReferences |
611
|
|
|
if icon["name"] in doc.icons: |
612
|
|
|
icons.append( |
613
|
|
|
{ |
614
|
|
|
"name": icon["name"], |
615
|
|
|
"color": lower_color, |
616
|
|
|
"link": icon["link"], |
617
|
|
|
} |
618
|
|
|
) |
619
|
|
|
else: |
620
|
|
|
debug( |
621
|
|
|
f"[WARNING] pandoc-latex-tip: {icon['name']}" |
622
|
|
|
" is not a correct icon name" |
623
|
|
|
) |
624
|
|
|
except FileNotFoundError: |
625
|
|
|
debug("[WARNING] pandoc-latex-tip: error in accessing to icons definition") |
626
|
|
|
|
627
|
|
|
|
628
|
|
|
# pylint:disable=too-many-return-statements |
629
|
|
|
def get_prefix(position: str, odd: bool = True) -> str: |
630
|
|
|
""" |
631
|
|
|
Get the latex prefix. |
632
|
|
|
|
633
|
|
|
Parameters |
634
|
|
|
---------- |
635
|
|
|
position |
636
|
|
|
The icon position |
637
|
|
|
odd |
638
|
|
|
Is the page is odd ? |
639
|
|
|
|
640
|
|
|
Returns |
641
|
|
|
------- |
642
|
|
|
str |
643
|
|
|
The latex prefix. |
644
|
|
|
""" |
645
|
|
|
if position == "right": |
646
|
|
|
if odd: |
647
|
|
|
return "\\pandoclatextipoddright" |
648
|
|
|
return "\\pandoclatextipevenright" |
649
|
|
|
if position in ("left", ""): |
650
|
|
|
if odd: |
651
|
|
|
return "\\pandoclatextipoddleft" |
652
|
|
|
return "\\pandoclatextipevenleft" |
653
|
|
|
if position == "inner": |
654
|
|
|
if odd: |
655
|
|
|
return "\\pandoclatextipoddinner" |
656
|
|
|
return "\\pandoclatextipeveninner" |
657
|
|
|
if position == "outer": |
658
|
|
|
if odd: |
659
|
|
|
return "\\pandoclatextipoddouter" |
660
|
|
|
return "\\pandoclatextipevenouter" |
661
|
|
|
debug( |
662
|
|
|
f"[WARNING] pandoc-latex-tip: {position}" |
663
|
|
|
" is not a correct position; using left" |
664
|
|
|
) |
665
|
|
|
if odd: |
666
|
|
|
return "\\pandoclatextipoddleft" |
667
|
|
|
return "\\pandoclatextipevenleft" |
668
|
|
|
|
669
|
|
|
|
670
|
|
|
def get_size(size: str) -> str: |
671
|
|
|
""" |
672
|
|
|
Get the correct size. |
673
|
|
|
|
674
|
|
|
Parameters |
675
|
|
|
---------- |
676
|
|
|
size |
677
|
|
|
The initial size |
678
|
|
|
|
679
|
|
|
Returns |
680
|
|
|
------- |
681
|
|
|
str |
682
|
|
|
The correct size. |
683
|
|
|
""" |
684
|
|
|
try: |
685
|
|
|
int_value = int(size) |
686
|
|
|
if int_value > 0: |
687
|
|
|
size = str(int_value) |
688
|
|
|
else: |
689
|
|
|
debug( |
690
|
|
|
f"[WARNING] pandoc-latex-tip: size must be greater than 0; using {size}" |
691
|
|
|
) |
692
|
|
|
except ValueError: |
693
|
|
|
debug(f"[WARNING] pandoc-latex-tip: size must be a number; using {size}") |
694
|
|
|
return size |
695
|
|
|
|
696
|
|
|
|
697
|
|
|
def create_images(doc: Doc, icons: list[dict[str, Any]], size: str) -> list[str]: |
698
|
|
|
""" |
699
|
|
|
Create the images. |
700
|
|
|
|
701
|
|
|
Parameters |
702
|
|
|
---------- |
703
|
|
|
doc |
704
|
|
|
The original document |
705
|
|
|
icons |
706
|
|
|
A list of icon definitions |
707
|
|
|
size |
708
|
|
|
The icon size. |
709
|
|
|
|
710
|
|
|
Returns |
711
|
|
|
------- |
712
|
|
|
list[str] |
713
|
|
|
A list of latex code. |
714
|
|
|
""" |
715
|
|
|
# Generate the LaTeX image code |
716
|
|
|
images = [] |
717
|
|
|
|
718
|
|
|
for icon in icons: |
719
|
|
|
# Get the image from the App cache folder |
720
|
|
|
# noinspection PyUnresolvedReferences |
721
|
|
|
image = os.path.join(doc.folder, icon["color"], icon["name"] + ".png") |
722
|
|
|
|
723
|
|
|
# Create the image if not existing in the cache |
724
|
|
|
try: |
725
|
|
|
if not os.path.isfile(image): |
726
|
|
|
# Create the image in the cache |
727
|
|
|
# noinspection PyUnresolvedReferences |
728
|
|
|
doc.icons[icon["name"]].export_icon( |
729
|
|
|
icon["name"], |
730
|
|
|
512, |
731
|
|
|
color=icon["color"], |
732
|
|
|
export_dir=os.path.join(doc.folder, icon["color"]), |
733
|
|
|
) |
734
|
|
|
|
735
|
|
|
# Add the LaTeX image |
736
|
|
|
image = Image( |
737
|
|
|
url=str(image), attributes={"width": size + "pt", "height": size + "pt"} |
738
|
|
|
) |
739
|
|
|
if icon["link"] == "": |
740
|
|
|
elem = image |
741
|
|
|
else: |
742
|
|
|
elem = Link(image, url=icon["link"]) |
743
|
|
|
images.append( |
744
|
|
|
convert_text( |
745
|
|
|
Plain(elem), input_format="panflute", output_format="latex" |
746
|
|
|
) |
747
|
|
|
) |
748
|
|
|
except TypeError: |
749
|
|
|
debug( |
750
|
|
|
f"[WARNING] pandoc-latex-tip: icon name " |
751
|
|
|
f"{icon['name']} does not exist" |
752
|
|
|
) |
753
|
|
|
except FileNotFoundError: |
754
|
|
|
debug("[WARNING] pandoc-latex-tip: error in generating image") |
755
|
|
|
|
756
|
|
|
return images |
757
|
|
|
|
758
|
|
|
|
759
|
|
|
def add_definition(doc: Doc, definition: dict[str, Any]) -> None: |
760
|
|
|
""" |
761
|
|
|
Add definition to document. |
762
|
|
|
|
763
|
|
|
Parameters |
764
|
|
|
---------- |
765
|
|
|
doc |
766
|
|
|
The original document |
767
|
|
|
definition |
768
|
|
|
The definition |
769
|
|
|
""" |
770
|
|
|
# Get the classes |
771
|
|
|
classes = definition["classes"] |
772
|
|
|
|
773
|
|
|
# Add a definition if correct |
774
|
|
|
if bool(classes): |
775
|
|
|
latex = latex_code( |
776
|
|
|
doc, |
777
|
|
|
definition, |
778
|
|
|
{ |
779
|
|
|
"icon": "icons", |
780
|
|
|
"position": "position", |
781
|
|
|
"size": "size", |
782
|
|
|
"color": "color", |
783
|
|
|
"link": "link", |
784
|
|
|
}, |
785
|
|
|
) |
786
|
|
|
if latex: |
787
|
|
|
# noinspection PyUnresolvedReferences |
788
|
|
|
doc.defined.append({"classes": set(classes), "latex": latex}) |
789
|
|
|
|
790
|
|
|
|
791
|
|
|
def prepare(doc: Doc) -> None: |
792
|
|
|
""" |
793
|
|
|
Prepare the document. |
794
|
|
|
|
795
|
|
|
Parameters |
796
|
|
|
---------- |
797
|
|
|
doc |
798
|
|
|
The original document. |
799
|
|
|
""" |
800
|
|
|
# Add getIconFont library to doc |
801
|
|
|
doc.icons = load_icons() |
802
|
|
|
|
803
|
|
|
# Prepare the definitions |
804
|
|
|
doc.defined = [] |
805
|
|
|
|
806
|
|
|
# Prepare the folder |
807
|
|
|
try: |
808
|
|
|
# Use user cache dir if possible |
809
|
|
|
doc.folder = platformdirs.AppDirs( |
810
|
|
|
"pandoc_latex_tip", |
811
|
|
|
).user_cache_dir |
812
|
|
|
if not pathlib.Path(doc.folder).exists(): |
813
|
|
|
pathlib.Path(doc.folder).mkdir(parents=True) |
814
|
|
|
except PermissionError: |
815
|
|
|
# Fallback to a temporary dir |
816
|
|
|
doc.folder = tempfile.mkdtemp( |
817
|
|
|
prefix="pandoc_latex_tip_", |
818
|
|
|
suffix="_cache", |
819
|
|
|
) |
820
|
|
|
|
821
|
|
|
# Get the meta data |
822
|
|
|
# noinspection PyUnresolvedReferences |
823
|
|
|
meta = doc.get_metadata("pandoc-latex-tip") |
824
|
|
|
|
825
|
|
|
if isinstance(meta, list): |
826
|
|
|
# Loop on all definitions |
827
|
|
|
for definition in meta: |
828
|
|
|
# Verify the definition |
829
|
|
|
if ( |
830
|
|
|
isinstance(definition, dict) |
831
|
|
|
and "classes" in definition |
832
|
|
|
and isinstance(definition["classes"], list) |
833
|
|
|
): |
834
|
|
|
add_definition(doc, definition) |
835
|
|
|
|
836
|
|
|
|
837
|
|
|
def finalize(doc: Doc) -> None: |
838
|
|
|
""" |
839
|
|
|
Finalize the document. |
840
|
|
|
|
841
|
|
|
Parameters |
842
|
|
|
---------- |
843
|
|
|
doc |
844
|
|
|
The original document |
845
|
|
|
""" |
846
|
|
|
# Add header-includes if necessary |
847
|
|
|
if "header-includes" not in doc.metadata: |
848
|
|
|
doc.metadata["header-includes"] = MetaList() |
849
|
|
|
# Convert header-includes to MetaList if necessary |
850
|
|
|
elif not isinstance(doc.metadata["header-includes"], MetaList): |
851
|
|
|
doc.metadata["header-includes"] = MetaList(doc.metadata["header-includes"]) |
852
|
|
|
|
853
|
|
|
doc.metadata["header-includes"].append( |
854
|
|
|
MetaInlines(RawInline("\\usepackage{needspace}", "tex")) |
855
|
|
|
) |
856
|
|
|
doc.metadata["header-includes"].append( |
857
|
|
|
MetaInlines(RawInline("\\usepackage{graphicx,grffile}", "tex")) |
858
|
|
|
) |
859
|
|
|
doc.metadata["header-includes"].append( |
860
|
|
|
MetaInlines(RawInline("\\usepackage{marginnote}", "tex")) |
861
|
|
|
) |
862
|
|
|
doc.metadata["header-includes"].append( |
863
|
|
|
MetaInlines(RawInline("\\usepackage{etoolbox}", "tex")) |
864
|
|
|
) |
865
|
|
|
doc.metadata["header-includes"].append( |
866
|
|
|
MetaInlines(RawInline("\\usepackage[strict]{changepage}", "tex")) |
867
|
|
|
) |
868
|
|
|
doc.metadata["header-includes"].append( |
869
|
|
|
MetaInlines( |
870
|
|
|
RawInline( |
871
|
|
|
r""" |
872
|
|
|
\makeatletter% |
873
|
|
|
\newcommand{\pandoclatextipoddinner}{\reversemarginpar}% |
874
|
|
|
\newcommand{\pandoclatextipeveninner}{\reversemarginpar}% |
875
|
|
|
\newcommand{\pandoclatextipoddouter}{\normalmarginpar}% |
876
|
|
|
\newcommand{\pandoclatextipevenouter}{\normalmarginpar}% |
877
|
|
|
\newcommand{\pandoclatextipoddleft}{\reversemarginpar}% |
878
|
|
|
\newcommand{\pandoclatextipoddright}{\normalmarginpar}% |
879
|
|
|
\if@twoside% |
880
|
|
|
\newcommand{\pandoclatextipevenright}{\reversemarginpar}% |
881
|
|
|
\newcommand{\pandoclatextipevenleft}{\normalmarginpar}% |
882
|
|
|
\else% |
883
|
|
|
\newcommand{\pandoclatextipevenright}{\normalmarginpar}% |
884
|
|
|
\newcommand{\pandoclatextipevenleft}{\reversemarginpar}% |
885
|
|
|
\fi% |
886
|
|
|
\makeatother% |
887
|
|
|
\checkoddpage |
888
|
|
|
""", |
889
|
|
|
"tex", |
890
|
|
|
) |
891
|
|
|
) |
892
|
|
|
) |
893
|
|
|
|
894
|
|
|
|
895
|
|
|
def main(doc: Doc | None = None) -> Doc: |
896
|
|
|
""" |
897
|
|
|
Transform the pandoc document. |
898
|
|
|
|
899
|
|
|
Arguments |
900
|
|
|
--------- |
901
|
|
|
doc |
902
|
|
|
The pandoc document |
903
|
|
|
|
904
|
|
|
Returns |
905
|
|
|
------- |
906
|
|
|
Doc |
907
|
|
|
The transformed document |
908
|
|
|
""" |
909
|
|
|
return run_filter(tip, prepare=prepare, finalize=finalize, doc=doc) |
910
|
|
|
|
911
|
|
|
|
912
|
|
|
class NewCommand(Command): |
913
|
|
|
""" |
914
|
|
|
NewCommand. |
915
|
|
|
""" |
916
|
|
|
|
917
|
|
|
name = "{format}" |
918
|
|
|
description = "Add new collection" |
919
|
|
|
|
920
|
|
|
def handle(self) -> int: |
921
|
|
|
""" |
922
|
|
|
Handle new command. |
923
|
|
|
|
924
|
|
|
Returns |
925
|
|
|
------- |
926
|
|
|
int |
927
|
|
|
status code |
928
|
|
|
""" |
929
|
|
|
self.line("New") |
930
|
|
|
return 0 |
931
|
|
|
|
932
|
|
|
|
933
|
|
|
class PandocLaTeXFilterCommand(Command): |
934
|
|
|
""" |
935
|
|
|
PandocLaTeXFilterCommand. |
936
|
|
|
""" |
937
|
|
|
|
938
|
|
|
name = "latex" |
939
|
|
|
description = "Run pandoc filter for LaTeX document" |
940
|
|
|
|
941
|
|
|
def handle(self) -> int: |
942
|
|
|
""" |
943
|
|
|
Handle latex command. |
944
|
|
|
|
945
|
|
|
Returns |
946
|
|
|
------- |
947
|
|
|
int |
948
|
|
|
status code |
949
|
|
|
""" |
950
|
|
|
main() |
951
|
|
|
return 0 |
952
|
|
|
|
953
|
|
|
|
954
|
|
|
class PandocBeamerFilterCommand(Command): |
955
|
|
|
""" |
956
|
|
|
PandocBeamerFilterCommand. |
957
|
|
|
""" |
958
|
|
|
|
959
|
|
|
name = "beamer" |
960
|
|
|
description = "Run pandoc filter for Beamer document" |
961
|
|
|
|
962
|
|
|
def handle(self) -> int: |
963
|
|
|
""" |
964
|
|
|
Handle beamer command. |
965
|
|
|
|
966
|
|
|
Returns |
967
|
|
|
------- |
968
|
|
|
int |
969
|
|
|
status code |
970
|
|
|
""" |
971
|
|
|
main() |
972
|
|
|
return 0 |
973
|
|
|
|
974
|
|
|
|
975
|
|
|
def app() -> None: |
976
|
|
|
""" |
977
|
|
|
Create a cleo application. |
978
|
|
|
""" |
979
|
|
|
application = Application("Pandoc LaTeX Tip Filter") |
980
|
|
|
application.add(NewCommand()) |
981
|
|
|
application.add(PandocLaTeXFilterCommand()) |
982
|
|
|
application.add(PandocBeamerFilterCommand()) |
983
|
|
|
application.run() |
984
|
|
|
|
985
|
|
|
|
986
|
|
|
if __name__ == "__main__": |
987
|
|
|
main() |
988
|
|
|
|