pandoc_latex_tip._main.tip()   B
last analyzed

Complexity

Conditions 7

Size

Total Lines 49
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 21
dl 0
loc 49
rs 7.9759
c 0
b 0
f 0
cc 7
nop 2
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