Passed
Push — develop ( 90694e...64aa5a )
by Christophe
01:01
created

pandoc_latex_tip._main.get_icons()   C

Complexity

Conditions 10

Size

Total Lines 83
Code Lines 41

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 41
dl 0
loc 83
rs 5.9999
c 0
b 0
f 0
cc 10
nop 3

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like pandoc_latex_tip._main.get_icons() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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
439
        # It is a CodeBlock: create a minipage to ensure the
440
        # _tip to be on the same page as the codeblock
441
        if isinstance(elem, CodeBlock):
442
            return [
443
                RawBlock(f"\\begin{{minipage}}{{\\textwidth}}{latex}", "tex"),
444
                elem,
445
                RawBlock("\\end{minipage}", "tex"),
446
            ]
447
448
        while elem.content and isinstance(elem.content[0], Div):
449
            elem = elem.content[0]
450
451
        if not elem.content or isinstance(
452
            elem.content[0], HorizontalRule | Figure | RawBlock | DefinitionList
453
        ):
454
            elem.content.insert(0, RawBlock(latex, "tex"))
455
            return None
456
        if isinstance(elem.content[0], Plain | Para):
457
            elem.content[0].content.insert(1, RawInline(latex, "tex"))
458
            return None
459
        if isinstance(elem.content[0], LineBlock):
460
            elem.content[0].content[0].content.insert(1, RawInline(latex, "tex"))
461
            return None
462
        if isinstance(elem.content[0], CodeBlock):
463
            elem.content.insert(
464
                0,
465
                RawBlock(f"\\begin{{minipage}}{{\\textwidth}}{latex}", "tex"),
466
            )
467
            elem.content.insert(2, RawBlock("\\end{minipage}", "tex"))
468
            return None
469
        if isinstance(elem.content[0], BulletList | OrderedList):
470
            elem.content[0].content[0].content[0].content.insert(
471
                1,
472
                RawInline(latex, "tex"),
473
            )
474
            return None
475
        debug("[WARNING] pandoc-latex-tip: Bad usage")
476
    return None
477
478
479
# pylint: disable=too-many-arguments,too-many-locals
480
def latex_code(doc: Doc, definition: dict[str, Any], keys: dict[str, str]) -> str:
481
    """
482
    Get the latex code.
483
484
    Parameters
485
    ----------
486
    doc
487
        The original document
488
    definition
489
        The defition
490
    keys
491
        Key mapping
492
493
    Returns
494
    -------
495
    str
496
        The latex code.
497
    """
498
    # Get the size
499
    size = get_size(str(definition.get(keys["size"], "18")))
500
501
    # Get the prefixes
502
    # noinspection PyArgumentEqualDefault
503
    prefix_odd = get_prefix_odd(str(definition.get(keys["position"], "")))
504
    prefix_even = get_prefix_even(str(definition.get(keys["position"], "")))
505
506
    # Get the icons
507
    icons = get_icons(doc, definition, keys)
508
509
    # Get the images
510
    images = create_images(doc, icons, size)
511
512
    if bool(images):
513
        # pylint: disable=consider-using-f-string
514
        return f"""
515
\\checkoddpage%%
516
\\ifoddpage%%
517
{prefix_odd}%%
518
\\else%%
519
{prefix_even}%%
520
\\fi%%
521
\\marginnote{{{''.join(images)}}}[0pt]\\vspace{{0cm}}%%
522
"""
523
524
    return ""
525
526
527
def get_icons(
528
    doc: Doc,
529
    definition: dict[str, Any],
530
    keys: dict[str, str],
531
) -> list[dict[str, Any]]:
532
    """
533
    Get tge icons.
534
535
    Parameters
536
    ----------
537
    doc
538
        The original document
539
    definition
540
        The definition
541
    keys
542
        Key mapping
543
544
    Returns
545
    -------
546
    list[dict[str, Any]]
547
        A list of icon definitions.
548
    """
549
    # Get the link
550
    link = str(definition.get(keys["link"], ""))
551
552
    # Get the default color
553
    color = str(definition.get(keys["color"], "black"))
554
555
    # Test the icons definition
556
    if keys["icon"] in definition:
557
        icons: list[dict[str, str]] = []
558
        # pylint: disable=invalid-name
559
        if isinstance(definition[keys["icon"]], list):
560
            for icon in definition[keys["icon"]]:
561
                if isinstance(icon, dict):
562
                    icon["link"] = icon.get("link", link)
563
                    if not icon.get("image"):
564
                        icon["color"] = icon.get("color", color)
565
                    add_icon(doc, icons, icon)
566
                else:
567
                    add_icon(
568
                        doc,
569
                        icons,
570
                        {
571
                            "name": icon,
572
                            "color": color,
573
                            "link": link,
574
                        },
575
                    )
576
        elif definition[keys["icon"]] in doc.icons:
577
            icons = [
578
                {
579
                    "name": definition[keys["icon"]],
580
                    "color": color,
581
                    "link": link,
582
                }
583
            ]
584
        elif definition.get(keys["image"]):
585
            icons = [
586
                {
587
                    "image": definition[keys["image"]],
588
                    "link": link,
589
                }
590
            ]
591
        else:
592
            icons = []
593
    elif keys.get("image") and keys.get("image") in definition:
594
        icons = [
595
            {
596
                "image": definition[keys["image"]],
597
                "link": link,
598
            }
599
        ]
600
    else:
601
        icons = [
602
            {
603
                "name": "fa-exclamation-circle",
604
                "color": color,
605
                "link": link,
606
            }
607
        ]
608
609
    return icons
610
611
612
def add_icon(doc: Doc, icons: list[dict[str, str]], icon: dict[str, str]) -> None:
613
    """
614
    Add icon.
615
616
    Parameters
617
    ----------
618
    doc
619
        The original document.
620
    icons
621
        A list of icon definition
622
    icon
623
        A potential new icon
624
    """
625
    if "image" in icon:
626
        icons.append(
627
            {
628
                "image": icon["image"],
629
                "link": icon["link"],
630
            }
631
        )
632
    else:
633
        if "name" not in icon:
634
            # Bad formed icon
635
            debug("[WARNING] pandoc-latex-tip: Bad formed icon")
636
            return
637
638
        # Lower the color
639
        lower_color = icon["color"].lower()
640
641
        # Convert the color to black if unexisting
642
        # pylint: disable=import-outside-toplevel
643
644
        if lower_color not in PIL.ImageColor.colormap:
645
            debug(
646
                f"[WARNING] pandoc-latex-tip: {lower_color}"
647
                " is not a correct color name; using black"
648
            )
649
            lower_color = "black"
650
651
        # Is the icon correct?
652
        try:
653
            # noinspection PyUnresolvedReferences
654
            if icon["name"] in doc.icons:
655
                icons.append(
656
                    {
657
                        "name": icon["name"],
658
                        "color": lower_color,
659
                        "link": icon["link"],
660
                    }
661
                )
662
            else:
663
                debug(
664
                    f"[WARNING] pandoc-latex-tip: {icon['name']}"
665
                    " is not a correct icon name"
666
                )
667
        except FileNotFoundError:
668
            debug("[WARNING] pandoc-latex-tip: error in accessing to icons definition")
669
670
671
# pylint:disable=too-many-return-statements
672
def get_prefix_odd(position: str) -> str:
673
    """
674
    Get the latex prefix.
675
676
    Parameters
677
    ----------
678
    position
679
        The icon position
680
681
    Returns
682
    -------
683
    str
684
        The latex prefix.
685
    """
686
    if position == "right":
687
        return "\\pandoclatextipoddright"
688
    if position in ("left", ""):
689
        return "\\pandoclatextipoddleft"
690
    if position == "inner":
691
        return "\\pandoclatextipoddinner"
692
    if position == "outer":
693
        return "\\pandoclatextipoddouter"
694
    debug(
695
        f"[WARNING] pandoc-latex-tip: {position}"
696
        " is not a correct position; using left"
697
    )
698
    return "\\pandoclatextipoddleft"
699
700
701
def get_prefix_even(position: str) -> str:
702
    """
703
    Get the latex prefix.
704
705
    Parameters
706
    ----------
707
    position
708
        The icon position
709
710
    Returns
711
    -------
712
    str
713
        The latex prefix.
714
    """
715
    if position == "right":
716
        return "\\pandoclatextipevenright"
717
    if position in ("left", ""):
718
        return "\\pandoclatextipevenleft"
719
    if position == "inner":
720
        return "\\pandoclatextipeveninner"
721
    if position == "outer":
722
        return "\\pandoclatextipevenouter"
723
    debug(
724
        f"[WARNING] pandoc-latex-tip: {position}"
725
        " is not a correct position; using left"
726
    )
727
    return "\\pandoclatextipevenleft"
728
729
730
def get_size(size: str) -> str:
731
    """
732
    Get the correct size.
733
734
    Parameters
735
    ----------
736
    size
737
        The initial size
738
739
    Returns
740
    -------
741
    str
742
        The correct size.
743
    """
744
    try:
745
        int_value = int(size)
746
        if int_value > 0:
747
            size = str(int_value)
748
        else:
749
            debug(
750
                f"[WARNING] pandoc-latex-tip: size must be greater than 0; using {size}"
751
            )
752
    except ValueError:
753
        debug(f"[WARNING] pandoc-latex-tip: size must be a number; using {size}")
754
    return size
755
756
757
def create_images(doc: Doc, icons: list[dict[str, Any]], size: str) -> list[str]:
758
    """
759
    Create the images.
760
761
    Parameters
762
    ----------
763
    doc
764
        The original document
765
    icons
766
        A list of icon definitions
767
    size
768
        The icon size.
769
770
    Returns
771
    -------
772
    list[str]
773
        A list of latex code.
774
    """
775
    # Generate the LaTeX image code
776
    images = []
777
778
    for icon in icons:
779
        # Get the image from the App cache folder
780
        # noinspection PyUnresolvedReferences
781
        if size.isdigit():
782
            size += "pt"
783
        if icon.get("image"):
784
            image = Image(
785
                url=str(icon.get("image")),
786
                attributes={"height": size},
787
            )
788
            elem = image if icon["link"] == "" else Link(image, url=icon["link"])
789
            images.append(
790
                convert_text(
791
                    Plain(elem), input_format="panflute", output_format="latex"
792
                )
793
            )
794
        else:
795
            image_path = path.join(doc.folder, icon["color"], icon["name"] + ".png")
796
797
            # Create the image if not existing in the cache
798
            try:
799
                if not path.isfile(image_path):
800
                    # Create the image in the cache
801
                    # noinspection PyUnresolvedReferences
802
                    doc.icons[icon["name"]].export_icon(
803
                        icon["name"],
804
                        512,
805
                        color=icon["color"],
806
                        export_dir=path.join(doc.folder, icon["color"]),
807
                    )
808
809
                # Add the LaTeX image
810
                image = Image(
811
                    url=str(image_path),
812
                    attributes={"height": size},
813
                )
814
                elem = image if icon["link"] == "" else Link(image, url=icon["link"])
815
                images.append(
816
                    convert_text(
817
                        Plain(elem), input_format="panflute", output_format="latex"
818
                    )
819
                )
820
            except TypeError:
821
                debug(
822
                    f"[WARNING] pandoc-latex-tip: icon name "
823
                    f"{icon['name']} does not exist"
824
                )
825
            except FileNotFoundError:
826
                debug("[WARNING] pandoc-latex-tip: error in generating image")
827
828
    return images
829
830
831
def add_definition(doc: Doc, definition: dict[str, Any]) -> None:
832
    """
833
    Add definition to document.
834
835
    Parameters
836
    ----------
837
    doc
838
        The original document
839
    definition
840
        The definition
841
    """
842
    # Get the classes
843
    classes = definition["classes"]
844
845
    # Add a definition if correct
846
    if bool(classes):
847
        latex = latex_code(
848
            doc,
849
            definition,
850
            {
851
                "icon": "icons",
852
                "position": "position",
853
                "size": "size",
854
                "color": "color",
855
                "link": "link",
856
            },
857
        )
858
        if latex:
859
            # noinspection PyUnresolvedReferences
860
            doc.defined.append({"classes": set(classes), "latex": latex})
861
862
863
def prepare(doc: Doc) -> None:
864
    """
865
    Prepare the document.
866
867
    Parameters
868
    ----------
869
    doc
870
        The original document.
871
    """
872
    # Add getIconFont library to doc
873
    doc.icons = load_icons()
874
875
    # Prepare the definitions
876
    doc.defined = []
877
878
    # Prepare the folder
879
    try:
880
        # Use user cache dir if possible
881
        doc.folder = platformdirs.AppDirs(
882
            "pandoc_latex_tip",
883
        ).user_cache_dir
884
        if not pathlib.Path(doc.folder).exists():
885
            pathlib.Path(doc.folder).mkdir(parents=True)
886
    except PermissionError:
887
        # Fallback to a temporary dir
888
        doc.folder = tempfile.mkdtemp(
889
            prefix="pandoc_latex_tip_",
890
            suffix="_cache",
891
        )
892
893
    # Get the meta data
894
    # noinspection PyUnresolvedReferences
895
    meta = doc.get_metadata("pandoc-latex-tip")
896
897
    if isinstance(meta, list):
898
        # Loop on all definitions
899
        for definition in meta:
900
            # Verify the definition
901
            if (
902
                isinstance(definition, dict)
903
                and "classes" in definition
904
                and isinstance(definition["classes"], list)
905
            ):
906
                add_definition(doc, definition)
907
908
909
def finalize(doc: Doc) -> None:
910
    """
911
    Finalize the document.
912
913
    Parameters
914
    ----------
915
    doc
916
        The original document
917
    """
918
    # Add header-includes if necessary
919
    if "header-includes" not in doc.metadata:
920
        doc.metadata["header-includes"] = MetaList()
921
    # Convert header-includes to MetaList if necessary
922
    elif not isinstance(doc.metadata["header-includes"], MetaList):
923
        doc.metadata["header-includes"] = MetaList(doc.metadata["header-includes"])
924
925
    doc.metadata["header-includes"].append(
926
        MetaInlines(RawInline("\\usepackage{graphicx,grffile}", "tex"))
927
    )
928
    doc.metadata["header-includes"].append(
929
        MetaInlines(RawInline("\\usepackage{marginnote}", "tex"))
930
    )
931
    doc.metadata["header-includes"].append(
932
        MetaInlines(RawInline("\\usepackage{etoolbox}", "tex"))
933
    )
934
    doc.metadata["header-includes"].append(
935
        MetaInlines(RawInline("\\usepackage[strict]{changepage}", "tex"))
936
    )
937
    doc.metadata["header-includes"].append(
938
        MetaInlines(
939
            RawInline(
940
                r"""
941
\makeatletter%
942
\newcommand{\pandoclatextipoddinner}{\reversemarginpar}%
943
\newcommand{\pandoclatextipeveninner}{\reversemarginpar}%
944
\newcommand{\pandoclatextipoddouter}{\normalmarginpar}%
945
\newcommand{\pandoclatextipevenouter}{\normalmarginpar}%
946
\newcommand{\pandoclatextipoddleft}{\reversemarginpar}%
947
\newcommand{\pandoclatextipoddright}{\normalmarginpar}%
948
\if@twoside%
949
\newcommand{\pandoclatextipevenright}{\reversemarginpar}%
950
\newcommand{\pandoclatextipevenleft}{\normalmarginpar}%
951
\else%
952
\newcommand{\pandoclatextipevenright}{\normalmarginpar}%
953
\newcommand{\pandoclatextipevenleft}{\reversemarginpar}%
954
\fi%
955
\makeatother%
956
\checkoddpage
957
    """,
958
                "tex",
959
            )
960
        )
961
    )
962
963
964
def main(doc: Doc | None = None) -> Doc:
965
    """
966
    Transform the pandoc document.
967
968
    Arguments
969
    ---------
970
    doc
971
        The pandoc document
972
973
    Returns
974
    -------
975
    Doc
976
        The transformed document
977
    """
978
    return run_filter(tip, prepare=prepare, finalize=finalize, doc=doc)
979
980
981
if __name__ == "__main__":
982
    main()
983