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