Passed
Push — develop ( fc1ac2...034d2d )
by Christophe
01:03
created

pandoc_latex_tip._main   F

Complexity

Total Complexity 97

Size/Duplication

Total Lines 950
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 97
eloc 441
dl 0
loc 950
rs 2
c 0
b 0
f 0

15 Functions

Rating   Name   Duplication   Size   Complexity  
D add_latex() 0 60 12
A get_core_icons() 0 27 1
B tip() 0 48 6
C load_icons() 0 67 9
A latex_code() 0 51 2
A add_definition() 0 30 3
B create_images() 0 57 6
A main() 0 15 1
B add_icon() 0 49 5
A get_prefix_odd() 0 27 5
A get_size() 0 25 3
A get_prefix_even() 0 27 5
B prepare() 0 44 8
B get_icons() 0 68 6
A finalize() 0 50 3

3 Methods

Rating   Name   Duplication   Size   Complexity  
A IconFont.__init__() 0 9 1
C IconFont.export_icon() 0 126 10
C IconFont.load_css() 0 52 11

How to fix   Complexity   

Complexity

Complex classes like pandoc_latex_tip._main 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
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:
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(
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_odd(str(definition.get(keys["position"], "")))
506
    prefix_even = get_prefix_even(str(definition.get(keys["position"], "")))
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
                    add_icon(doc, icons, icon)
570
                except AttributeError:
571
                    add_icon(
572
                        doc,
573
                        icons,
574
                        {
575
                            "name": icon,
576
                            "color": color,
577
                            "link": link,
578
                        },
579
                    )
580
        elif definition[key_icons] in doc.icons:
581
            icons = [
582
                {
583
                    "name": definition[key_icons],
584
                    "color": color,
585
                    "link": link,
586
                }
587
            ]
588
        else:
589
            icons = []
590
    else:
591
        icons = [
592
            {
593
                "name": "fa-exclamation-circle",
594
                "color": color,
595
                "link": link,
596
            }
597
        ]
598
599
    return icons
600
601
602
def add_icon(doc: Doc, icons: list[dict[str, str]], icon: dict[str, str]) -> None:
603
    """
604
    Add icon.
605
606
    Parameters
607
    ----------
608
    doc
609
        The original document.
610
    icons
611
        A list of icon definition
612
    icon
613
        A potential new icon
614
    """
615
    if "name" not in icon:
616
        # Bad formed icon
617
        debug("[WARNING] pandoc-latex-tip: Bad formed icon")
618
        return
619
620
    # Lower the color
621
    lower_color = icon["color"].lower()
622
623
    # Convert the color to black if unexisting
624
    # pylint: disable=import-outside-toplevel
625
626
    if lower_color not in PIL.ImageColor.colormap:
627
        debug(
628
            f"[WARNING] pandoc-latex-tip: {lower_color}"
629
            " is not a correct color name; using black"
630
        )
631
        lower_color = "black"
632
633
    # Is the icon correct?
634
    try:
635
        # noinspection PyUnresolvedReferences
636
        if icon["name"] in doc.icons:
637
            icons.append(
638
                {
639
                    "name": icon["name"],
640
                    "color": lower_color,
641
                    "link": icon["link"],
642
                }
643
            )
644
        else:
645
            debug(
646
                f"[WARNING] pandoc-latex-tip: {icon['name']}"
647
                " is not a correct icon name"
648
            )
649
    except FileNotFoundError:
650
        debug("[WARNING] pandoc-latex-tip: error in accessing to icons definition")
651
652
653
# pylint:disable=too-many-return-statements
654
def get_prefix_odd(position: str) -> str:
655
    """
656
    Get the latex prefix.
657
658
    Parameters
659
    ----------
660
    position
661
        The icon position
662
663
    Returns
664
    -------
665
    str
666
        The latex prefix.
667
    """
668
    if position == "right":
669
        return "\\pandoclatextipoddright"
670
    if position in ("left", ""):
671
        return "\\pandoclatextipoddleft"
672
    if position == "inner":
673
        return "\\pandoclatextipoddinner"
674
    if position == "outer":
675
        return "\\pandoclatextipoddouter"
676
    debug(
677
        f"[WARNING] pandoc-latex-tip: {position}"
678
        " is not a correct position; using left"
679
    )
680
    return "\\pandoclatextipoddleft"
681
682
683
def get_prefix_even(position: str) -> str:
684
    """
685
    Get the latex prefix.
686
687
    Parameters
688
    ----------
689
    position
690
        The icon position
691
692
    Returns
693
    -------
694
    str
695
        The latex prefix.
696
    """
697
    if position == "right":
698
        return "\\pandoclatextipevenright"
699
    if position in ("left", ""):
700
        return "\\pandoclatextipevenleft"
701
    if position == "inner":
702
        return "\\pandoclatextipeveninner"
703
    if position == "outer":
704
        return "\\pandoclatextipevenouter"
705
    debug(
706
        f"[WARNING] pandoc-latex-tip: {position}"
707
        " is not a correct position; using left"
708
    )
709
    return "\\pandoclatextipevenleft"
710
711
712
def get_size(size: str) -> str:
713
    """
714
    Get the correct size.
715
716
    Parameters
717
    ----------
718
    size
719
        The initial size
720
721
    Returns
722
    -------
723
    str
724
        The correct size.
725
    """
726
    try:
727
        int_value = int(size)
728
        if int_value > 0:
729
            size = str(int_value)
730
        else:
731
            debug(
732
                f"[WARNING] pandoc-latex-tip: size must be greater than 0; using {size}"
733
            )
734
    except ValueError:
735
        debug(f"[WARNING] pandoc-latex-tip: size must be a number; using {size}")
736
    return size
737
738
739
def create_images(doc: Doc, icons: list[dict[str, Any]], size: str) -> list[str]:
740
    """
741
    Create the images.
742
743
    Parameters
744
    ----------
745
    doc
746
        The original document
747
    icons
748
        A list of icon definitions
749
    size
750
        The icon size.
751
752
    Returns
753
    -------
754
    list[str]
755
        A list of latex code.
756
    """
757
    # Generate the LaTeX image code
758
    images = []
759
760
    for icon in icons:
761
        # Get the image from the App cache folder
762
        # noinspection PyUnresolvedReferences
763
        image = path.join(doc.folder, icon["color"], icon["name"] + ".png")
764
765
        # Create the image if not existing in the cache
766
        try:
767
            if not path.isfile(image):
768
                # Create the image in the cache
769
                # noinspection PyUnresolvedReferences
770
                doc.icons[icon["name"]].export_icon(
771
                    icon["name"],
772
                    512,
773
                    color=icon["color"],
774
                    export_dir=path.join(doc.folder, icon["color"]),
775
                )
776
777
            # Add the LaTeX image
778
            image = Image(
779
                url=str(image), attributes={"width": size + "pt", "height": size + "pt"}
780
            )
781
            elem = image if icon["link"] == "" else Link(image, url=icon["link"])
782
            images.append(
783
                convert_text(
784
                    Plain(elem), input_format="panflute", output_format="latex"
785
                )
786
            )
787
        except TypeError:
788
            debug(
789
                f"[WARNING] pandoc-latex-tip: icon name "
790
                f"{icon['name']} does not exist"
791
            )
792
        except FileNotFoundError:
793
            debug("[WARNING] pandoc-latex-tip: error in generating image")
794
795
    return images
796
797
798
def add_definition(doc: Doc, definition: dict[str, Any]) -> None:
799
    """
800
    Add definition to document.
801
802
    Parameters
803
    ----------
804
    doc
805
        The original document
806
    definition
807
        The definition
808
    """
809
    # Get the classes
810
    classes = definition["classes"]
811
812
    # Add a definition if correct
813
    if bool(classes):
814
        latex = latex_code(
815
            doc,
816
            definition,
817
            {
818
                "icon": "icons",
819
                "position": "position",
820
                "size": "size",
821
                "color": "color",
822
                "link": "link",
823
            },
824
        )
825
        if latex:
826
            # noinspection PyUnresolvedReferences
827
            doc.defined.append({"classes": set(classes), "latex": latex})
828
829
830
def prepare(doc: Doc) -> None:
831
    """
832
    Prepare the document.
833
834
    Parameters
835
    ----------
836
    doc
837
        The original document.
838
    """
839
    # Add getIconFont library to doc
840
    doc.icons = load_icons()
841
842
    # Prepare the definitions
843
    doc.defined = []
844
845
    # Prepare the folder
846
    try:
847
        # Use user cache dir if possible
848
        doc.folder = platformdirs.AppDirs(
849
            "pandoc_latex_tip",
850
        ).user_cache_dir
851
        if not pathlib.Path(doc.folder).exists():
852
            pathlib.Path(doc.folder).mkdir(parents=True)
853
    except PermissionError:
854
        # Fallback to a temporary dir
855
        doc.folder = tempfile.mkdtemp(
856
            prefix="pandoc_latex_tip_",
857
            suffix="_cache",
858
        )
859
860
    # Get the meta data
861
    # noinspection PyUnresolvedReferences
862
    meta = doc.get_metadata("pandoc-latex-tip")
863
864
    if isinstance(meta, list):
865
        # Loop on all definitions
866
        for definition in meta:
867
            # Verify the definition
868
            if (
869
                isinstance(definition, dict)
870
                and "classes" in definition
871
                and isinstance(definition["classes"], list)
872
            ):
873
                add_definition(doc, definition)
874
875
876
def finalize(doc: Doc) -> None:
877
    """
878
    Finalize the document.
879
880
    Parameters
881
    ----------
882
    doc
883
        The original document
884
    """
885
    # Add header-includes if necessary
886
    if "header-includes" not in doc.metadata:
887
        doc.metadata["header-includes"] = MetaList()
888
    # Convert header-includes to MetaList if necessary
889
    elif not isinstance(doc.metadata["header-includes"], MetaList):
890
        doc.metadata["header-includes"] = MetaList(doc.metadata["header-includes"])
891
892
    doc.metadata["header-includes"].append(
893
        MetaInlines(RawInline("\\usepackage{graphicx,grffile}", "tex"))
894
    )
895
    doc.metadata["header-includes"].append(
896
        MetaInlines(RawInline("\\usepackage{marginnote}", "tex"))
897
    )
898
    doc.metadata["header-includes"].append(
899
        MetaInlines(RawInline("\\usepackage{etoolbox}", "tex"))
900
    )
901
    doc.metadata["header-includes"].append(
902
        MetaInlines(RawInline("\\usepackage[strict]{changepage}", "tex"))
903
    )
904
    doc.metadata["header-includes"].append(
905
        MetaInlines(
906
            RawInline(
907
                r"""
908
\makeatletter%
909
\newcommand{\pandoclatextipoddinner}{\reversemarginpar}%
910
\newcommand{\pandoclatextipeveninner}{\reversemarginpar}%
911
\newcommand{\pandoclatextipoddouter}{\normalmarginpar}%
912
\newcommand{\pandoclatextipevenouter}{\normalmarginpar}%
913
\newcommand{\pandoclatextipoddleft}{\reversemarginpar}%
914
\newcommand{\pandoclatextipoddright}{\normalmarginpar}%
915
\if@twoside%
916
\newcommand{\pandoclatextipevenright}{\reversemarginpar}%
917
\newcommand{\pandoclatextipevenleft}{\normalmarginpar}%
918
\else%
919
\newcommand{\pandoclatextipevenright}{\normalmarginpar}%
920
\newcommand{\pandoclatextipevenleft}{\reversemarginpar}%
921
\fi%
922
\makeatother%
923
\checkoddpage
924
    """,
925
                "tex",
926
            )
927
        )
928
    )
929
930
931
def main(doc: Doc | None = None) -> Doc:
932
    """
933
    Transform the pandoc document.
934
935
    Arguments
936
    ---------
937
    doc
938
        The pandoc document
939
940
    Returns
941
    -------
942
    Doc
943
        The transformed document
944
    """
945
    return run_filter(tip, prepare=prepare, finalize=finalize, doc=doc)
946
947
948
if __name__ == "__main__":
949
    main()
950