1
|
|
|
#!/usr/bin/env python |
2
|
|
|
|
3
|
|
|
""" |
4
|
|
|
Pandoc filter for adding tip in LaTeX. |
5
|
|
|
""" |
6
|
|
|
|
7
|
|
|
from __future__ import annotations |
8
|
|
|
|
9
|
|
|
import operator |
10
|
|
|
import pathlib |
11
|
|
|
import re |
12
|
|
|
import sys |
13
|
|
|
import tempfile |
14
|
|
|
from os import path |
15
|
|
|
from typing import Any |
16
|
|
|
|
17
|
|
|
import PIL.Image |
18
|
|
|
import PIL.ImageColor |
19
|
|
|
import PIL.ImageDraw |
20
|
|
|
import PIL.ImageFont |
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 greatly 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
|
|
|
common = ( |
120
|
|
|
name |
121
|
|
|
if common is None |
122
|
|
|
else path.commonprefix((common, name)) # type: ignore |
123
|
|
|
) |
124
|
|
|
icons[name] = character |
125
|
|
|
break |
126
|
|
|
|
127
|
|
|
common = common or "" |
128
|
|
|
|
129
|
|
|
# Remove common prefix |
130
|
|
|
if prefix: |
131
|
|
|
icons = { |
132
|
|
|
prefix + name[len(common) :]: value for name, value in icons.items() |
133
|
|
|
} |
134
|
|
|
|
135
|
|
|
return dict(sorted(icons.items(), key=operator.itemgetter(0))) |
136
|
|
|
|
137
|
|
|
# pylint: disable=too-many-arguments,too-many-locals,too-many-positional-arguments |
138
|
|
|
def export_icon( |
139
|
|
|
self, |
140
|
|
|
icon: str, |
141
|
|
|
size: int, |
142
|
|
|
color: str = "black", |
143
|
|
|
scale: float | str = "auto", |
144
|
|
|
filename: str | None = None, |
145
|
|
|
export_dir: str = "exported", |
146
|
|
|
) -> None: |
147
|
|
|
""" |
148
|
|
|
Export given icon with provided parameters. |
149
|
|
|
|
150
|
|
|
If the desired icon size is less than 150x150 pixels, we will first |
151
|
|
|
create a 150x150 pixels image and then scale it down, so that |
152
|
|
|
it's much less likely that the edges of the icon end up cropped. |
153
|
|
|
|
154
|
|
|
Parameters |
155
|
|
|
---------- |
156
|
|
|
icon |
157
|
|
|
valid icon name |
158
|
|
|
size |
159
|
|
|
icon size in pixels |
160
|
|
|
color |
161
|
|
|
color name or hex value |
162
|
|
|
scale |
163
|
|
|
scaling factor between 0 and 1, or 'auto' for automatic scaling |
164
|
|
|
filename |
165
|
|
|
name of the output file |
166
|
|
|
export_dir |
167
|
|
|
path to export directory |
168
|
|
|
""" |
169
|
|
|
org_size = size |
170
|
|
|
size = max(150, size) |
171
|
|
|
|
172
|
|
|
image = PIL.Image.new("RGBA", (size, size), color=(0, 0, 0, 0)) |
173
|
|
|
draw = PIL.ImageDraw.Draw(image) |
174
|
|
|
|
175
|
|
|
scale_factor = 1.0 if scale == "auto" else float(scale) |
176
|
|
|
|
177
|
|
|
font_size = int(size * scale_factor) |
178
|
|
|
font = PIL.ImageFont.truetype(self.ttf_file, font_size) |
179
|
|
|
width = draw.textlength(self.css_icons[icon], font=font) |
180
|
|
|
height = font_size # always, as long as single-line of text |
181
|
|
|
|
182
|
|
|
# If auto-scaling is enabled, we need to make sure the resulting |
183
|
|
|
# graphic fits inside the boundary. The values are rounded and may be |
184
|
|
|
# off by a pixel or two, so we may need to do a few iterations. |
185
|
|
|
# The use of a decrementing multiplication factor protects us from |
186
|
|
|
# getting into an infinite loop. |
187
|
|
|
if scale == "auto": |
188
|
|
|
iteration = 0 |
189
|
|
|
factor = 1.0 |
190
|
|
|
|
191
|
|
|
while True: |
192
|
|
|
width = draw.textlength(self.css_icons[icon], font=font) |
193
|
|
|
|
194
|
|
|
# Check if the image fits |
195
|
|
|
dim = max(width, height) |
196
|
|
|
if dim > size: |
197
|
|
|
font = PIL.ImageFont.truetype( |
198
|
|
|
self.ttf_file, |
199
|
|
|
int(size * size / dim * factor), |
200
|
|
|
) |
201
|
|
|
else: |
202
|
|
|
break |
203
|
|
|
|
204
|
|
|
# Adjust the factor every two iterations |
205
|
|
|
iteration += 1 |
206
|
|
|
if iteration % 2 == 0: |
207
|
|
|
factor *= 0.99 |
208
|
|
|
|
209
|
|
|
draw.text( |
210
|
|
|
((size - width) / 2, (size - height) / 2), |
211
|
|
|
self.css_icons[icon], |
212
|
|
|
font=font, |
213
|
|
|
fill=color, |
214
|
|
|
anchor="lt", |
215
|
|
|
) |
216
|
|
|
|
217
|
|
|
# Get bounding box |
218
|
|
|
bbox = image.getbbox() |
219
|
|
|
|
220
|
|
|
# Create an alpha mask |
221
|
|
|
image_mask = PIL.Image.new("L", (size, size)) |
222
|
|
|
draw_mask = PIL.ImageDraw.Draw(image_mask) |
223
|
|
|
|
224
|
|
|
# Draw the icon on the mask |
225
|
|
|
draw_mask.text( |
226
|
|
|
((size - width) / 2, (size - height) / 2), |
227
|
|
|
self.css_icons[icon], |
228
|
|
|
font=font, |
229
|
|
|
fill=255, |
230
|
|
|
anchor="lt", |
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) |
241
|
|
|
border_h = int((size - (bbox[3] - bbox[1])) / 2) |
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(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 or "latex-tip-image" in elem.attributes: |
388
|
|
|
return add_latex( |
389
|
|
|
elem, |
390
|
|
|
latex_code( |
391
|
|
|
doc, |
392
|
|
|
elem.attributes, |
393
|
|
|
{ |
394
|
|
|
"icon": "latex-tip-icon", |
395
|
|
|
"image": "latex-tip-image", |
396
|
|
|
"position": "latex-tip-position", |
397
|
|
|
"size": "latex-tip-size", |
398
|
|
|
"color": "latex-tip-color", |
399
|
|
|
"link": "latex-tip-link", |
400
|
|
|
}, |
401
|
|
|
), |
402
|
|
|
) |
403
|
|
|
|
404
|
|
|
# Get the classes |
405
|
|
|
classes = set(elem.classes) |
406
|
|
|
|
407
|
|
|
# Loop on all font size definition |
408
|
|
|
# noinspection PyUnresolvedReferences |
409
|
|
|
for definition in doc.defined: |
410
|
|
|
# Are the classes correct? |
411
|
|
|
if classes >= definition["classes"]: |
412
|
|
|
return add_latex(elem, definition["latex"]) |
413
|
|
|
|
414
|
|
|
return None |
415
|
|
|
|
416
|
|
|
|
417
|
|
|
def add_latex(elem: Element, latex: str) -> list[Element] | None: |
418
|
|
|
""" |
419
|
|
|
Add latex code. |
420
|
|
|
|
421
|
|
|
Parameters |
422
|
|
|
---------- |
423
|
|
|
elem |
424
|
|
|
Current element |
425
|
|
|
latex |
426
|
|
|
Latex code |
427
|
|
|
|
428
|
|
|
Returns |
429
|
|
|
------- |
430
|
|
|
list[Element] | None |
431
|
|
|
The additional elements if any. |
432
|
|
|
""" |
433
|
|
|
# pylint: disable=too-many-return-statements |
434
|
|
|
if bool(latex): |
435
|
|
|
# Is it a Span or a Code? |
436
|
|
|
if isinstance(elem, Span | Code): |
437
|
|
|
return [elem, RawInline(latex, "tex")] |
438
|
|
|
if isinstance(elem, CodeBlock): |
439
|
|
|
return [RawBlock(latex, "tex"), elem] |
440
|
|
|
while elem.content and isinstance(elem.content[0], Div): |
441
|
|
|
elem = elem.content[0] |
442
|
|
|
if not elem.content or isinstance( |
443
|
|
|
elem.content[0], |
444
|
|
|
HorizontalRule | Figure | RawBlock | DefinitionList | CodeBlock, |
445
|
|
|
): |
446
|
|
|
elem.content.insert(0, RawBlock(latex, "tex")) |
447
|
|
|
return None |
448
|
|
|
if isinstance(elem.content[0], Plain | Para): |
449
|
|
|
elem.content[0].content.insert(1, RawInline(latex, "tex")) |
450
|
|
|
return None |
451
|
|
|
if isinstance(elem.content[0], LineBlock): |
452
|
|
|
elem.content[0].content[0].content.insert(1, RawInline(latex, "tex")) |
453
|
|
|
return None |
454
|
|
|
if isinstance(elem.content[0], BulletList | OrderedList): |
455
|
|
|
elem.content[0].content[0].content[0].content.insert( |
456
|
|
|
1, |
457
|
|
|
RawInline(latex, "tex"), |
458
|
|
|
) |
459
|
|
|
return None |
460
|
|
|
debug("[WARNING] pandoc-latex-tip: Bad usage") |
461
|
|
|
return None |
462
|
|
|
|
463
|
|
|
|
464
|
|
|
# pylint: disable=too-many-arguments,too-many-locals |
465
|
|
|
def latex_code(doc: Doc, definition: dict[str, Any], keys: dict[str, str]) -> str: |
466
|
|
|
""" |
467
|
|
|
Get the latex code. |
468
|
|
|
|
469
|
|
|
Parameters |
470
|
|
|
---------- |
471
|
|
|
doc |
472
|
|
|
The original document |
473
|
|
|
definition |
474
|
|
|
The defition |
475
|
|
|
keys |
476
|
|
|
Key mapping |
477
|
|
|
|
478
|
|
|
Returns |
479
|
|
|
------- |
480
|
|
|
str |
481
|
|
|
The latex code. |
482
|
|
|
""" |
483
|
|
|
# Get the size |
484
|
|
|
size = get_size(str(definition.get(keys["size"], "18"))) |
485
|
|
|
|
486
|
|
|
# Get the prefixes |
487
|
|
|
# noinspection PyArgumentEqualDefault |
488
|
|
|
prefix_odd = get_prefix_odd(str(definition.get(keys["position"], ""))) |
489
|
|
|
prefix_even = get_prefix_even(str(definition.get(keys["position"], ""))) |
490
|
|
|
|
491
|
|
|
# Get the icons |
492
|
|
|
icons = get_icons(doc, definition, keys) |
493
|
|
|
|
494
|
|
|
# Get the images |
495
|
|
|
images = create_images(doc, icons, size) |
496
|
|
|
|
497
|
|
|
if bool(images): |
498
|
|
|
# pylint: disable=consider-using-f-string |
499
|
|
|
return f""" |
500
|
|
|
\\checkoddpage%% |
501
|
|
|
\\ifoddpage%% |
502
|
|
|
{prefix_odd}%% |
503
|
|
|
\\else%% |
504
|
|
|
{prefix_even}%% |
505
|
|
|
\\fi%% |
506
|
|
|
\\marginnote{{{''.join(images)}}}[0pt]\\vspace{{0cm}}%% |
507
|
|
|
""" |
508
|
|
|
|
509
|
|
|
return "" |
510
|
|
|
|
511
|
|
|
|
512
|
|
|
def get_icons( |
513
|
|
|
doc: Doc, |
514
|
|
|
definition: dict[str, Any], |
515
|
|
|
keys: dict[str, str], |
516
|
|
|
) -> list[dict[str, Any]]: |
517
|
|
|
""" |
518
|
|
|
Get tge icons. |
519
|
|
|
|
520
|
|
|
Parameters |
521
|
|
|
---------- |
522
|
|
|
doc |
523
|
|
|
The original document |
524
|
|
|
definition |
525
|
|
|
The definition |
526
|
|
|
keys |
527
|
|
|
Key mapping |
528
|
|
|
|
529
|
|
|
Returns |
530
|
|
|
------- |
531
|
|
|
list[dict[str, Any]] |
532
|
|
|
A list of icon definitions. |
533
|
|
|
""" |
534
|
|
|
# Get the link |
535
|
|
|
link = str(definition.get(keys["link"], "")) |
536
|
|
|
|
537
|
|
|
# Get the default color |
538
|
|
|
color = str(definition.get(keys["color"], "black")) |
539
|
|
|
|
540
|
|
|
# Test the icons definition |
541
|
|
|
if keys["icon"] in definition: |
542
|
|
|
icons: list[dict[str, str]] = [] |
543
|
|
|
# pylint: disable=invalid-name |
544
|
|
|
if isinstance(definition[keys["icon"]], list): |
545
|
|
|
for icon in definition[keys["icon"]]: |
546
|
|
|
if isinstance(icon, dict): |
547
|
|
|
icon["link"] = icon.get("link", link) |
548
|
|
|
if not icon.get("image"): |
549
|
|
|
icon["color"] = icon.get("color", color) |
550
|
|
|
add_icon(doc, icons, icon) |
551
|
|
|
else: |
552
|
|
|
add_icon( |
553
|
|
|
doc, |
554
|
|
|
icons, |
555
|
|
|
{ |
556
|
|
|
"name": icon, |
557
|
|
|
"color": color, |
558
|
|
|
"link": link, |
559
|
|
|
}, |
560
|
|
|
) |
561
|
|
|
elif definition[keys["icon"]] in doc.icons: |
562
|
|
|
icons = [ |
563
|
|
|
{ |
564
|
|
|
"name": definition[keys["icon"]], |
565
|
|
|
"color": color, |
566
|
|
|
"link": link, |
567
|
|
|
} |
568
|
|
|
] |
569
|
|
|
elif definition.get(keys["image"]): |
570
|
|
|
icons = [ |
571
|
|
|
{ |
572
|
|
|
"image": definition[keys["image"]], |
573
|
|
|
"link": link, |
574
|
|
|
} |
575
|
|
|
] |
576
|
|
|
else: |
577
|
|
|
icons = [] |
578
|
|
|
elif keys.get("image") and keys.get("image") in definition: |
579
|
|
|
icons = [ |
580
|
|
|
{ |
581
|
|
|
"image": definition[keys["image"]], |
582
|
|
|
"link": link, |
583
|
|
|
} |
584
|
|
|
] |
585
|
|
|
else: |
586
|
|
|
icons = [ |
587
|
|
|
{ |
588
|
|
|
"name": "fa-exclamation-circle", |
589
|
|
|
"color": color, |
590
|
|
|
"link": link, |
591
|
|
|
} |
592
|
|
|
] |
593
|
|
|
|
594
|
|
|
return icons |
595
|
|
|
|
596
|
|
|
|
597
|
|
|
def add_icon(doc: Doc, icons: list[dict[str, str]], icon: dict[str, str]) -> None: |
598
|
|
|
""" |
599
|
|
|
Add icon. |
600
|
|
|
|
601
|
|
|
Parameters |
602
|
|
|
---------- |
603
|
|
|
doc |
604
|
|
|
The original document. |
605
|
|
|
icons |
606
|
|
|
A list of icon definition |
607
|
|
|
icon |
608
|
|
|
A potential new icon |
609
|
|
|
""" |
610
|
|
|
if "image" in icon: |
611
|
|
|
icons.append( |
612
|
|
|
{ |
613
|
|
|
"image": icon["image"], |
614
|
|
|
"link": icon["link"], |
615
|
|
|
} |
616
|
|
|
) |
617
|
|
|
else: |
618
|
|
|
if "name" not in icon: |
619
|
|
|
# Bad formed icon |
620
|
|
|
debug("[WARNING] pandoc-latex-tip: Bad formed icon") |
621
|
|
|
return |
622
|
|
|
|
623
|
|
|
# Lower the color |
624
|
|
|
lower_color = icon["color"].lower() |
625
|
|
|
|
626
|
|
|
# Convert the color to black if unexisting |
627
|
|
|
# pylint: disable=import-outside-toplevel |
628
|
|
|
|
629
|
|
|
if lower_color not in PIL.ImageColor.colormap: |
630
|
|
|
debug( |
631
|
|
|
f"[WARNING] pandoc-latex-tip: {lower_color}" |
632
|
|
|
" is not a correct color name; using black" |
633
|
|
|
) |
634
|
|
|
lower_color = "black" |
635
|
|
|
|
636
|
|
|
# Is the icon correct? |
637
|
|
|
try: |
638
|
|
|
# noinspection PyUnresolvedReferences |
639
|
|
|
if icon["name"] in doc.icons: |
640
|
|
|
icons.append( |
641
|
|
|
{ |
642
|
|
|
"name": icon["name"], |
643
|
|
|
"color": lower_color, |
644
|
|
|
"link": icon["link"], |
645
|
|
|
} |
646
|
|
|
) |
647
|
|
|
else: |
648
|
|
|
debug( |
649
|
|
|
f"[WARNING] pandoc-latex-tip: {icon['name']}" |
650
|
|
|
" is not a correct icon name" |
651
|
|
|
) |
652
|
|
|
except FileNotFoundError: |
653
|
|
|
debug("[WARNING] pandoc-latex-tip: error in accessing to icons definition") |
654
|
|
|
|
655
|
|
|
|
656
|
|
|
# pylint:disable=too-many-return-statements |
657
|
|
|
def get_prefix_odd(position: str) -> str: |
658
|
|
|
""" |
659
|
|
|
Get the latex prefix. |
660
|
|
|
|
661
|
|
|
Parameters |
662
|
|
|
---------- |
663
|
|
|
position |
664
|
|
|
The icon position |
665
|
|
|
|
666
|
|
|
Returns |
667
|
|
|
------- |
668
|
|
|
str |
669
|
|
|
The latex prefix. |
670
|
|
|
""" |
671
|
|
|
if position == "right": |
672
|
|
|
return "\\PandocLatexTipOddRight" |
673
|
|
|
if position in ("left", ""): |
674
|
|
|
return "\\PandocLatexTipOddLeft" |
675
|
|
|
if position == "inner": |
676
|
|
|
return "\\PandocLatexTipOddInner" |
677
|
|
|
if position == "outer": |
678
|
|
|
return "\\PandocLatexTipOddOuter" |
679
|
|
|
debug( |
680
|
|
|
f"[WARNING] pandoc-latex-tip: {position}" |
681
|
|
|
" is not a correct position; using left" |
682
|
|
|
) |
683
|
|
|
return "\\PandocLatexTipOddLeft" |
684
|
|
|
|
685
|
|
|
|
686
|
|
|
def get_prefix_even(position: str) -> str: |
687
|
|
|
""" |
688
|
|
|
Get the latex prefix. |
689
|
|
|
|
690
|
|
|
Parameters |
691
|
|
|
---------- |
692
|
|
|
position |
693
|
|
|
The icon position |
694
|
|
|
|
695
|
|
|
Returns |
696
|
|
|
------- |
697
|
|
|
str |
698
|
|
|
The latex prefix. |
699
|
|
|
""" |
700
|
|
|
if position == "right": |
701
|
|
|
return "\\PandocLatexTipEvenRight" |
702
|
|
|
if position in ("left", ""): |
703
|
|
|
return "\\PandocLatexTipEvenLeft" |
704
|
|
|
if position == "inner": |
705
|
|
|
return "\\PandocLatexTipEvenInner" |
706
|
|
|
if position == "outer": |
707
|
|
|
return "\\PandocLatexTipEvenOuter" |
708
|
|
|
debug( |
709
|
|
|
f"[WARNING] pandoc-latex-tip: {position}" |
710
|
|
|
" is not a correct position; using left" |
711
|
|
|
) |
712
|
|
|
return "\\PandocLatexTipEvenLeft" |
713
|
|
|
|
714
|
|
|
|
715
|
|
|
def get_size(size: str) -> str: |
716
|
|
|
""" |
717
|
|
|
Get the correct size. |
718
|
|
|
|
719
|
|
|
Parameters |
720
|
|
|
---------- |
721
|
|
|
size |
722
|
|
|
The initial size |
723
|
|
|
|
724
|
|
|
Returns |
725
|
|
|
------- |
726
|
|
|
str |
727
|
|
|
The correct size. |
728
|
|
|
""" |
729
|
|
|
regex = re.compile("^(?P<length>\\d+(\\.\\d*)?)(pt|mm|cm|in|ex|em|mu|sp)?$") |
730
|
|
|
if regex.match(size): |
731
|
|
|
length = float(regex.match(size).group("length")) |
732
|
|
|
if length <= 0: |
733
|
|
|
debug("[WARNING] pandoc-latex-tip: size must be greater than 0; using 18") |
734
|
|
|
return "18" |
735
|
|
|
else: |
736
|
|
|
debug( |
737
|
|
|
"[WARNING] pandoc-latex-tip: size must be a correct LaTeX length; using 18" |
738
|
|
|
) |
739
|
|
|
return "18" |
740
|
|
|
return size |
741
|
|
|
|
742
|
|
|
|
743
|
|
|
def create_images(doc: Doc, icons: list[dict[str, Any]], size: str) -> list[str]: |
744
|
|
|
""" |
745
|
|
|
Create the images. |
746
|
|
|
|
747
|
|
|
Parameters |
748
|
|
|
---------- |
749
|
|
|
doc |
750
|
|
|
The original document |
751
|
|
|
icons |
752
|
|
|
A list of icon definitions |
753
|
|
|
size |
754
|
|
|
The icon size. |
755
|
|
|
|
756
|
|
|
Returns |
757
|
|
|
------- |
758
|
|
|
list[str] |
759
|
|
|
A list of latex code. |
760
|
|
|
""" |
761
|
|
|
# Generate the LaTeX image code |
762
|
|
|
images = [] |
763
|
|
|
|
764
|
|
|
for icon in icons: |
765
|
|
|
# Get the image from the App cache folder |
766
|
|
|
# noinspection PyUnresolvedReferences |
767
|
|
|
if size.isdigit(): |
768
|
|
|
size += "pt" |
769
|
|
|
if icon.get("image"): |
770
|
|
|
image = Image( |
771
|
|
|
url=str(icon.get("image")), |
772
|
|
|
attributes={"height": size}, |
773
|
|
|
) |
774
|
|
|
elem = image if icon["link"] == "" else Link(image, url=icon["link"]) |
775
|
|
|
images.append( |
776
|
|
|
convert_text( |
777
|
|
|
Plain(elem), input_format="panflute", output_format="latex" |
778
|
|
|
) |
779
|
|
|
) |
780
|
|
|
else: |
781
|
|
|
image_path = path.join(doc.folder, icon["color"], icon["name"] + ".png") |
782
|
|
|
|
783
|
|
|
# Create the image if not existing in the cache |
784
|
|
|
try: |
785
|
|
|
if not path.isfile(image_path): |
786
|
|
|
# Create the image in the cache |
787
|
|
|
# noinspection PyUnresolvedReferences |
788
|
|
|
doc.icons[icon["name"]].export_icon( |
789
|
|
|
icon["name"], |
790
|
|
|
512, |
791
|
|
|
color=icon["color"], |
792
|
|
|
export_dir=path.join(doc.folder, icon["color"]), |
793
|
|
|
) |
794
|
|
|
|
795
|
|
|
# Add the LaTeX image |
796
|
|
|
image = Image( |
797
|
|
|
url=str(image_path), |
798
|
|
|
attributes={"height": size}, |
799
|
|
|
) |
800
|
|
|
elem = image if icon["link"] == "" else Link(image, url=icon["link"]) |
801
|
|
|
images.append( |
802
|
|
|
convert_text( |
803
|
|
|
Plain(elem), input_format="panflute", output_format="latex" |
804
|
|
|
) |
805
|
|
|
) |
806
|
|
|
except TypeError: |
807
|
|
|
debug( |
808
|
|
|
f"[WARNING] pandoc-latex-tip: icon name " |
809
|
|
|
f"{icon['name']} does not exist" |
810
|
|
|
) |
811
|
|
|
except FileNotFoundError: |
812
|
|
|
debug("[WARNING] pandoc-latex-tip: error in generating image") |
813
|
|
|
|
814
|
|
|
return images |
815
|
|
|
|
816
|
|
|
|
817
|
|
|
def add_definition(doc: Doc, definition: dict[str, Any]) -> None: |
818
|
|
|
""" |
819
|
|
|
Add definition to document. |
820
|
|
|
|
821
|
|
|
Parameters |
822
|
|
|
---------- |
823
|
|
|
doc |
824
|
|
|
The original document |
825
|
|
|
definition |
826
|
|
|
The definition |
827
|
|
|
""" |
828
|
|
|
# Get the classes |
829
|
|
|
classes = definition["classes"] |
830
|
|
|
|
831
|
|
|
# Add a definition if correct |
832
|
|
|
if bool(classes): |
833
|
|
|
latex = latex_code( |
834
|
|
|
doc, |
835
|
|
|
definition, |
836
|
|
|
{ |
837
|
|
|
"icon": "icons", |
838
|
|
|
"position": "position", |
839
|
|
|
"size": "size", |
840
|
|
|
"color": "color", |
841
|
|
|
"link": "link", |
842
|
|
|
}, |
843
|
|
|
) |
844
|
|
|
if latex: |
845
|
|
|
# noinspection PyUnresolvedReferences |
846
|
|
|
doc.defined.append({"classes": set(classes), "latex": latex}) |
847
|
|
|
|
848
|
|
|
|
849
|
|
|
def prepare(doc: Doc) -> None: |
850
|
|
|
""" |
851
|
|
|
Prepare the document. |
852
|
|
|
|
853
|
|
|
Parameters |
854
|
|
|
---------- |
855
|
|
|
doc |
856
|
|
|
The original document. |
857
|
|
|
""" |
858
|
|
|
# Add getIconFont library to doc |
859
|
|
|
doc.icons = load_icons() |
860
|
|
|
|
861
|
|
|
# Prepare the definitions |
862
|
|
|
doc.defined = [] |
863
|
|
|
|
864
|
|
|
# Prepare the folder |
865
|
|
|
try: |
866
|
|
|
# Use user cache dir if possible |
867
|
|
|
doc.folder = platformdirs.AppDirs( |
868
|
|
|
"pandoc_latex_tip", |
869
|
|
|
).user_cache_dir |
870
|
|
|
if not pathlib.Path(doc.folder).exists(): |
871
|
|
|
pathlib.Path(doc.folder).mkdir(parents=True) |
872
|
|
|
except PermissionError: |
873
|
|
|
# Fallback to a temporary dir |
874
|
|
|
doc.folder = tempfile.mkdtemp( |
875
|
|
|
prefix="pandoc_latex_tip_", |
876
|
|
|
suffix="_cache", |
877
|
|
|
) |
878
|
|
|
|
879
|
|
|
# Get the meta data |
880
|
|
|
# noinspection PyUnresolvedReferences |
881
|
|
|
meta = doc.get_metadata("pandoc-latex-tip") |
882
|
|
|
|
883
|
|
|
if isinstance(meta, list): |
884
|
|
|
# Loop on all definitions |
885
|
|
|
for definition in meta: |
886
|
|
|
# Verify the definition |
887
|
|
|
if ( |
888
|
|
|
isinstance(definition, dict) |
889
|
|
|
and "classes" in definition |
890
|
|
|
and isinstance(definition["classes"], list) |
891
|
|
|
): |
892
|
|
|
add_definition(doc, definition) |
893
|
|
|
|
894
|
|
|
|
895
|
|
|
def finalize(doc: Doc) -> None: |
896
|
|
|
""" |
897
|
|
|
Finalize the document. |
898
|
|
|
|
899
|
|
|
Parameters |
900
|
|
|
---------- |
901
|
|
|
doc |
902
|
|
|
The original document |
903
|
|
|
""" |
904
|
|
|
# Add header-includes if necessary |
905
|
|
|
if "header-includes" not in doc.metadata: |
906
|
|
|
doc.metadata["header-includes"] = MetaList() |
907
|
|
|
# Convert header-includes to MetaList if necessary |
908
|
|
|
elif not isinstance(doc.metadata["header-includes"], MetaList): |
909
|
|
|
doc.metadata["header-includes"] = MetaList(doc.metadata["header-includes"]) |
910
|
|
|
|
911
|
|
|
doc.metadata["header-includes"].append( |
912
|
|
|
MetaInlines(RawInline("\\usepackage{graphicx,grffile}", "tex")) |
913
|
|
|
) |
914
|
|
|
doc.metadata["header-includes"].append( |
915
|
|
|
MetaInlines(RawInline("\\usepackage{marginnote}", "tex")) |
916
|
|
|
) |
917
|
|
|
doc.metadata["header-includes"].append( |
918
|
|
|
MetaInlines(RawInline("\\usepackage{etoolbox}", "tex")) |
919
|
|
|
) |
920
|
|
|
doc.metadata["header-includes"].append( |
921
|
|
|
MetaInlines(RawInline("\\usepackage[strict]{changepage}", "tex")) |
922
|
|
|
) |
923
|
|
|
doc.metadata["header-includes"].append( |
924
|
|
|
MetaInlines( |
925
|
|
|
RawInline( |
926
|
|
|
r""" |
927
|
|
|
\makeatletter% |
928
|
|
|
\newcommand{\PandocLatexTipOddInner}{\reversemarginpar}% |
929
|
|
|
\newcommand{\PandocLatexTipEvenInner}{\reversemarginpar}% |
930
|
|
|
\newcommand{\PandocLatexTipOddOuter}{\normalmarginpar}% |
931
|
|
|
\newcommand{\PandocLatexTipEvenOuter}{\normalmarginpar}% |
932
|
|
|
\newcommand{\PandocLatexTipOddLeft}{\reversemarginpar}% |
933
|
|
|
\newcommand{\PandocLatexTipOddRight}{\normalmarginpar}% |
934
|
|
|
\if@twoside% |
935
|
|
|
\newcommand{\PandocLatexTipEvenRight}{\reversemarginpar}% |
936
|
|
|
\newcommand{\PandocLatexTipEvenLeft}{\normalmarginpar}% |
937
|
|
|
\else% |
938
|
|
|
\newcommand{\PandocLatexTipEvenRight}{\normalmarginpar}% |
939
|
|
|
\newcommand{\PandocLatexTipEvenLeft}{\reversemarginpar}% |
940
|
|
|
\fi% |
941
|
|
|
\makeatother% |
942
|
|
|
\checkoddpage% |
943
|
|
|
""", |
944
|
|
|
"tex", |
945
|
|
|
) |
946
|
|
|
) |
947
|
|
|
) |
948
|
|
|
|
949
|
|
|
|
950
|
|
|
def main(doc: Doc | None = None) -> Doc: |
951
|
|
|
""" |
952
|
|
|
Transform the pandoc document. |
953
|
|
|
|
954
|
|
|
Arguments |
955
|
|
|
--------- |
956
|
|
|
doc |
957
|
|
|
The pandoc document |
958
|
|
|
|
959
|
|
|
Returns |
960
|
|
|
------- |
961
|
|
|
Doc |
962
|
|
|
The transformed document |
963
|
|
|
""" |
964
|
|
|
return run_filter(tip, prepare=prepare, finalize=finalize, doc=doc) |
965
|
|
|
|
966
|
|
|
|
967
|
|
|
if __name__ == "__main__": |
968
|
|
|
main() |
969
|
|
|
|