Passed
Push — develop ( 874d1e...76289b )
by Christophe
04:05
created

pandoc_latex_tip._main   F

Complexity

Total Complexity 98

Size/Duplication

Total Lines 988
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 98
eloc 450
dl 0
loc 988
rs 2
c 0
b 0
f 0

14 Functions

Rating   Name   Duplication   Size   Complexity  
D add_latex() 0 60 12
A add_definition() 0 30 3
B create_images() 0 60 6
A app() 0 9 1
A main() 0 15 1
B add_icon() 0 49 5
B tip() 0 48 6
A get_size() 0 25 3
C get_prefix() 0 39 10
B prepare() 0 44 8
B load_icons() 0 72 7
B get_icons() 0 67 6
A finalize() 0 53 3
A latex_code() 0 51 2

6 Methods

Rating   Name   Duplication   Size   Complexity  
A NewCommand.handle() 0 11 1
A IconFont.__init__() 0 9 1
C IconFont.export_icon() 0 127 10
C IconFont.load_css() 0 51 11
A PandocLaTeXFilterCommand.handle() 0 11 1
A PandocBeamerFilterCommand.handle() 0 11 1

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