Passed
Push — develop ( 97b560...fec923 )
by Christophe
01:08
created

pandoc_latex_tip._main.IconsDeleteCommand.handle()   B

Complexity

Conditions 7

Size

Total Lines 41
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

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