pandoc_latex_admonition._main.prepare()   C
last analyzed

Complexity

Conditions 9

Size

Total Lines 50
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 30
dl 0
loc 50
rs 6.6666
c 0
b 0
f 0
cc 9
nop 1
1
#!/usr/bin/env python
2
3
"""
4
Pandoc filter for adding admonition in LaTeX.
5
"""
6
7
import uuid
8
from typing import Any
9
10
from panflute import (
11
    Doc,
12
    Element,
13
    Figure,
14
    MetaBool,
15
    MetaInlines,
16
    MetaList,
17
    Note,
18
    RawBlock,
19
    RawInline,
20
    convert_text,
21
    debug,
22
    run_filter,
23
)
24
25
26
def default_environment() -> dict[str, Any]:
27
    """
28
    Get the default environment.
29
30
    Returns
31
    -------
32
    dict[str, Any]
33
        The default environment
34
    """
35
    return {
36
        "env": "env-" + str(uuid.uuid4()),
37
        "color": "black",
38
        "position": "left",
39
        "linewidth": 2,
40
        "margin": -4,
41
        "innermargin": 5,
42
        "localfootnotes": False,
43
        "nobreak": False,
44
    }
45
46
47
def x11colors() -> dict[str, str]:
48
    """
49
    Get the x11 colors.
50
51
    Returns
52
    -------
53
    dict[str, str]
54
        The x11 colors
55
    """
56
    # See https://www.w3.org/TR/css-color-3/#svg-color
57
    return {
58
        "aliceblue": "F0F8FF",
59
        "antiquewhite": "FAEBD7",
60
        "aqua": "00FFFF",
61
        "aquamarine": "7FFFD4",
62
        "azure": "F0FFFF",
63
        "beige": "F5F5DC",
64
        "bisque": "FFE4C4",
65
        "black": "000000",
66
        "blanchedalmond": "FFEBCD",
67
        "blue": "0000FF",
68
        "blueviolet": "8A2BE2",
69
        "brown": "A52A2A",
70
        "burlywood": "DEB887",
71
        "cadetblue": "5F9EA0",
72
        "chartreuse": "7FFF00",
73
        "chocolate": "D2691E",
74
        "coral": "FF7F50",
75
        "cornflowerblue": "6495ED",
76
        "cornsilk": "FFF8DC",
77
        "crimson": "DC143C",
78
        "cyan": "00FFFF",
79
        "darkblue": "00008B",
80
        "darkcyan": "008B8B",
81
        "darkgoldenrod": "B8860B",
82
        "darkgray": "A9A9A9",
83
        "darkgreen": "006400",
84
        "darkgrey": "A9A9A9",
85
        "darkkhaki": "BDB76B",
86
        "darkmagenta": "8B008B",
87
        "darkolivegreen": "556B2F",
88
        "darkorange": "FF8C00",
89
        "darkorchid": "9932CC",
90
        "darkred": "8B0000",
91
        "darksalmon": "E9967A",
92
        "darkseagreen": "8FBC8F",
93
        "darkslateblue": "483D8B",
94
        "darkslategray": "2F4F4F",
95
        "darkslategrey": "2F4F4F",
96
        "darkturquoise": "00CED1",
97
        "darkviolet": "9400D3",
98
        "deeppink": "FF1493",
99
        "deepskyblue": "00BFFF",
100
        "dimgray": "696969",
101
        "dimgrey": "696969",
102
        "dodgerblue": "1E90FF",
103
        "firebrick": "B22222",
104
        "floralwhite": "FFFAF0",
105
        "forestgreen": "228B22",
106
        "fuchsia": "FF00FF",
107
        "gainsboro": "DCDCDC",
108
        "ghostwhite": "F8F8FF",
109
        "gold": "FFD700",
110
        "goldenrod": "DAA520",
111
        "gray": "808080",
112
        "green": "008000",
113
        "greenyellow": "ADFF2F",
114
        "grey": "808080",
115
        "honeydew": "F0FFF0",
116
        "hotpink": "FF69B4",
117
        "indianred": "CD5C5C",
118
        "indigo": "4B0082",
119
        "ivory": "FFFFF0",
120
        "khaki": "F0E68C",
121
        "lavender": "E6E6FA",
122
        "lavenderblush": "FFF0F5",
123
        "lawngreen": "7CFC00",
124
        "lemonchiffon": "FFFACD",
125
        "lightblue": "ADD8E6",
126
        "lightcoral": "F08080",
127
        "lightcyan": "E0FFFF",
128
        "lightgoldenrodyellow": "FAFAD2",
129
        "lightgray": "D3D3D3",
130
        "lightgreen": "90EE90",
131
        "lightgrey": "D3D3D3",
132
        "lightpink": "FFB6C1",
133
        "lightsalmon": "FFA07A",
134
        "lightseagreen": "20B2AA",
135
        "lightskyblue": "87CEFA",
136
        "lightslategray": "778899",
137
        "lightslategrey": "778899",
138
        "lightsteelblue": "B0C4DE",
139
        "lightyellow": "FFFFE0",
140
        "lime": "00FF00",
141
        "limegreen": "32CD32",
142
        "linen": "FAF0E6",
143
        "magenta": "FF00FF",
144
        "maroon": "800000",
145
        "mediumaquamarine": "66CDAA",
146
        "mediumblue": "0000CD",
147
        "mediumorchid": "BA55D3",
148
        "mediumpurple": "9370DB",
149
        "mediumseagreen": "3CB371",
150
        "mediumslateblue": "7B68EE",
151
        "mediumspringgreen": "00FA9A",
152
        "mediumturquoise": "48D1CC",
153
        "mediumvioletred": "C71585",
154
        "midnightblue": "191970",
155
        "mintcream": "F5FFFA",
156
        "mistyrose": "FFE4E1",
157
        "moccasin": "FFE4B5",
158
        "navajowhite": "FFDEAD",
159
        "navy": "000080",
160
        "oldlace": "FDF5E6",
161
        "olive": "808000",
162
        "olivedrab": "6B8E23",
163
        "orange": "FFA500",
164
        "orangered": "FF4500",
165
        "orchid": "DA70D6",
166
        "palegoldenrod": "EEE8AA",
167
        "palegreen": "98FB98",
168
        "paleturquoise": "AFEEEE",
169
        "palevioletred": "DB7093",
170
        "papayawhip": "FFEFD5",
171
        "peachpuff": "FFDAB9",
172
        "peru": "CD853F",
173
        "pink": "FFC0CB",
174
        "plum": "DDA0DD",
175
        "powderblue": "B0E0E6",
176
        "purple": "800080",
177
        "red": "FF0000",
178
        "rosybrown": "BC8F8F",
179
        "royalblue": "4169E1",
180
        "saddlebrown": "8B4513",
181
        "salmon": "FA8072",
182
        "sandybrown": "F4A460",
183
        "seagreen": "2E8B57",
184
        "seashell": "FFF5EE",
185
        "sienna": "A0522D",
186
        "silver": "C0C0C0",
187
        "skyblue": "87CEEB",
188
        "slateblue": "6A5ACD",
189
        "slategray": "708090",
190
        "slategrey": "708090",
191
        "snow": "FFFAFA",
192
        "springgreen": "00FF7F",
193
        "steelblue": "4682B4",
194
        "tan": "D2B48C",
195
        "teal": "008080",
196
        "thistle": "D8BFD8",
197
        "tomato": "FF6347",
198
        "turquoise": "40E0D0",
199
        "violet": "EE82EE",
200
        "wheat": "F5DEB3",
201
        "white": "FFFFFF",
202
        "whitesmoke": "F5F5F5",
203
        "yellow": "FFFF00",
204
        "yellowgreen": "9ACD32",
205
    }
206
207
208
# pylint: disable=inconsistent-return-statements
209
def admonition(elem: Element, doc: Doc) -> Element | None:
210
    """
211
    Add admonition to elem.
212
213
    Arguments
214
    ---------
215
    elem
216
        The current element
217
    doc
218
        The pandoc document
219
220
    Returns
221
    -------
222
    Element | None
223
        The modified element or None
224
    """
225
    # Is it in the right format and is it Div or a CodeBlock?
226
    if doc.format in ("latex", "beamer") and elem.tag in ("Div", "CodeBlock"):
227
        # Is there a latex-admonition-color attribute?
228
        if "latex-admonition-color" in elem.attributes:
229
            environment = define_environment(
230
                doc,
231
                elem.attributes,
232
                "latex-admonition-color",
233
                "latex-admonition-position",
234
                "latex-admonition-linewidth",
235
                "latex-admonition-margin",
236
                "latex-admonition-innermargin",
237
                "latex-admonition-localfootnotes",
238
                "latex-admonition-nobreak",
239
            )
240
            doc.added.append(environment)
241
            return add_latex(elem, environment)
242
        # Get the classes
243
        classes = set(elem.classes)
244
245
        # Loop on all font size definition
246
        for environment in doc.defined:
247
            # Are the classes correct?
248
            if any(classes >= defined for defined in environment["classes"]):
249
                return add_latex(elem, environment)
250
    return None
251
252
253
def add_latex(elem: Element, environment: dict[str, Any]) -> Element | None:
254
    """
255
    Add LaTeX code to the element.
256
257
    Arguments
258
    ---------
259
    elem
260
        The current element
261
262
    environment
263
        The environment to add
264
265
    Returns
266
    -------
267
    Element | None
268
        The modified element
269
    """
270
271
    def note(element, doc):
272
        if (
273
            isinstance(element, Note)
274
            and doc.format == "beamer"
275
            and not environment["localfootnotes"]
276
        ):
277
            return RawInline(
278
                "".join(
279
                    [
280
                        "\\footnote<.->[frame]{",
281
                        convert_text(
282
                            element.content,
283
                            input_format="panflute",
284
                            output_format="latex",
285
                        ),
286
                        "}",
287
                    ]
288
                ),
289
                "tex",
290
            )
291
        return None
292
293
    if (
294
        elem.tag == "Div"
295
        and elem.content
296
        and elem.content[0].tag == "Div"
297
        and "title" in elem.content[0].classes
298
        and elem.content[0].content[0].tag == "Para"
299
    ):
300
        elem.content[0].content[0].content.insert(
301
            0,
302
            RawInline(f"\\textbf{{\\color{{{environment['color']}}}", "tex"),
303
        )
304
        elem.content[0].content[0].content.append(RawInline("}", "tex"))
305
306
    images = []
307
308
    def extract_images(element, _doc):
309
        # Extract image which is alone with a title
310
        if isinstance(element, Figure) and len(element.content) == 1:
311
            images.append(element)
312
            return []
313
        return None
314
315
    # The images need to be placed after the framed environment
316
    return [
317
        RawBlock("\\begin{" + environment["env"] + "}", "tex"),
318
        elem.walk(extract_images).walk(note),
319
        RawBlock("\\end{" + environment["env"] + "}", "tex"),
320
    ] + images
321
322
323
def prepare(doc: Doc) -> None:
324
    """
325
    Prepare the document.
326
327
    Arguments
328
    ---------
329
    doc
330
        The pandoc document
331
    """
332
    doc.x11colors = x11colors()
333
334
    # Prepare the definitions
335
    doc.defined = []
336
    doc.added = []
337
338
    # Get the meta data
339
    meta = doc.get_metadata("pandoc-latex-admonition")
340
341
    # pylint: disable=too-many-nested-blocks
342
    if isinstance(meta, list):
343
        # Loop on all definitions
344
        for definition in meta:
345
            # Verify the definition
346
            if (
347
                isinstance(definition, dict)
348
                and "classes" in definition
349
                and isinstance(definition["classes"], list)
350
            ):
351
                environment = define_environment(
352
                    doc,
353
                    definition,
354
                    "color",
355
                    "position",
356
                    "linewidth",
357
                    "margin",
358
                    "innermargin",
359
                    "localfootnotes",
360
                    "nobreak",
361
                )
362
                classes = []
363
                if all(isinstance(elem, str) for elem in definition["classes"]):
364
                    classes.append(set(definition["classes"]))
365
                else:
366
                    for elem in definition["classes"]:
367
                        if isinstance(elem, str):
368
                            classes.append({elem})
369
                        else:
370
                            classes.append({str(x) for x in elem})
371
                environment["classes"] = classes
372
                doc.defined.append(environment)
373
374
375
# pylint: disable=too-many-arguments,too-many-positional-arguments
376
def define_environment(
377
    doc: Doc,
378
    definition: dict[str, str],
379
    key_color: str,
380
    key_position: str,
381
    key_linewidth: str,
382
    key_margin: str,
383
    key_innermargin: str,
384
    key_localfootnotes: str,
385
    key_nobreak: str,
386
) -> dict[str, Any]:
387
    """
388
    Define a new environment.
389
390
    Arguments
391
    ---------
392
    doc
393
        The pandoc document
394
395
    definition
396
        The definition
397
398
    key_color
399
        The color key
400
401
    key_position
402
        The position key
403
404
    key_linewidth
405
        The linewidth key
406
407
    key_margin
408
        The margin key
409
410
    key_innermargin
411
        The innermargin key
412
413
    key_localfootnotes
414
        The localfootnotes key
415
416
    key_nobreak
417
        The nobreak key
418
419
    Returns
420
    -------
421
    dict[str, Any]
422
        A new environment
423
    """
424
    # Get the default environment
425
    environment = default_environment()
426
    define_color(environment, definition, key_color, doc=doc)
427
    define_position(environment, definition, key_position)
428
    define_linewidth(environment, definition, key_linewidth)
429
    define_margin(environment, definition, key_margin)
430
    define_innermargin(environment, definition, key_innermargin)
431
    define_localfootnotes(environment, definition, key_localfootnotes)
432
    define_nobreak(environment, definition, key_nobreak)
433
    return environment
434
435
436
def define_color(
437
    environment: dict[str, Any], definition: dict[str, str], key_color: str, doc: Doc
438
):
439
    """
440
    Define the color.
441
442
    Arguments
443
    ---------
444
    environment
445
        The environment
446
447
    definition
448
        The definition
449
450
    key_color
451
        The color key
452
453
    doc
454
        The pandoc document
455
    """
456
    if key_color in definition:
457
        color = definition[key_color].lower()
458
        if color in doc.x11colors:
459
            environment["color"] = color
460
        else:
461
            # color must be a valid x11 color
462
            # See https://www.w3.org/TR/css-color-3/#svg-color
463
            debug(
464
                "[WARNING] pandoc-latex-admonition: "
465
                + color
466
                + " is not a valid x11 color; using "
467
                + environment["color"]
468
            )
469
470
471
def define_position(
472
    environment: dict[str, Any], definition: dict[str, str], key_position: str
473
) -> None:
474
    """
475
    Define the position.
476
477
    Arguments
478
    ---------
479
    environment
480
        The environment
481
482
    definition
483
        The definition
484
485
    key_position
486
        The position key
487
    """
488
    if key_position in definition:
489
        environment["position"] = definition[key_position]
490
491
492
def define_linewidth(
493
    environment: dict[str, Any], definition: dict[str, str], key_linewidth: str
494
) -> None:
495
    """
496
    Define the line width.
497
498
    Arguments
499
    ---------
500
    environment
501
        The environment
502
503
    definition
504
        The definition
505
506
    key_linewidth
507
        The linewidth key
508
    """
509
    if key_linewidth in definition:
510
        try:
511
            linewidth = int(definition[key_linewidth])
512
            if linewidth <= 0:
513
                debug(
514
                    "[WARNING] pandoc-latex-admonition: "
515
                    + "linewidth must be a positivie integer; using "
516
                    + str(environment["linewidth"])
517
                )
518
            else:
519
                environment["linewidth"] = linewidth
520
        except ValueError:
521
            debug(
522
                "[WARNING] pandoc-latex-admonition: linewidth is not a valid; using "
523
                + str(environment["linewidth"])
524
            )
525
526
527
def define_margin(
528
    environment: dict[str, Any], definition: dict[str, str], key_margin: str
529
) -> None:
530
    """
531
    Define the margin.
532
533
    Arguments
534
    ---------
535
    environment
536
        The environment
537
538
    definition
539
        The definition
540
541
    key_margin
542
        The margin key
543
    """
544
    if key_margin in definition:
545
        try:
546
            environment["margin"] = int(definition[key_margin])
547
        except ValueError:
548
            debug(
549
                "[WARNING] pandoc-latex-admonition: margin is not a valid; using "
550
                + str(environment["margin"])
551
            )
552
553
554
def define_innermargin(
555
    environment: dict[str, Any], definition: dict[str, str], key_innermargin: str
556
) -> None:
557
    """
558
    Define the inner margin.
559
560
    Arguments
561
    ---------
562
    environment
563
        The environment
564
565
    definition
566
        The definition
567
568
    key_innermargin
569
        The inner margin key
570
    """
571
    if key_innermargin in definition:
572
        try:
573
            environment["innermargin"] = int(definition[key_innermargin])
574
        except ValueError:
575
            debug(
576
                "[WARNING] pandoc-latex-admonition: innermargin is not a valid; using "
577
                + str(environment["innermargin"])
578
            )
579
580
581
def define_localfootnotes(
582
    environment: dict[str, Any], definition: dict[str, str], key_localfootnotes: str
583
) -> None:
584
    """
585
    Define the local footnotes.
586
587
    Arguments
588
    ---------
589
    environment
590
        The environment
591
592
    definition
593
        The definition
594
595
    key_localfootnotes
596
        The localfootnotes key
597
    """
598
    if key_localfootnotes in definition:
599
        environment["localfootnotes"] = definition[key_localfootnotes].lower() == "true"
600
601
602
def define_nobreak(
603
    environment: dict[str, Any], definition: dict[str, str], key_nobreak: str
604
) -> None:
605
    """
606
    Define the nobreak.
607
608
    Arguments
609
    ---------
610
    environment
611
        The environment
612
613
    definition
614
        The definition
615
616
    key_nobreak
617
        The nobreak key
618
    """
619
    if key_nobreak in definition:
620
        environment["nobreak"] = (
621
            str(definition[key_nobreak]).lower() == "true"  # noqa: FURB123
622
        )
623
624
625
def new_environment(doc: Doc, environment: dict[str, Any]) -> str:
626
    """
627
    Create a new environment.
628
629
    Arguments
630
    ---------
631
    doc
632
        The pandoc document
633
634
    environment
635
        The environment
636
637
    Returns
638
    -------
639
    str
640
        The LaTeX environment
641
    """
642
    options = ["blanker"]
643
644
    if not environment["nobreak"]:
645
        options.append("breakable")
646
    if environment["position"] == "left":
647
        options.append(left_bar(environment))
648
    elif environment["position"] == "right":
649
        options.append(right_bar(environment))
650
    elif environment["position"] == "inner":
651
        options.append(
652
            f"if odd page={{{left_bar(environment)}}}{{{right_bar(environment)}}}"
653
        )
654
    elif environment["position"] == "outer":
655
        options.append(
656
            f"if odd page={{{right_bar(environment)}}}{{{left_bar(environment)}}}"
657
        )
658
    else:
659
        options.append(left_bar(environment))
660
661
    if environment["localfootnotes"] or doc.format == "beamer":
662
        return f"""
663
\\newenvironment{{{environment['env']}}}
664
{{
665
    \\tcolorbox[{','.join(options)}]
666
}}
667
{{
668
    \\endtcolorbox
669
}}
670
        """
671
    return f"""
672
\\newenvironment{{{environment['env']}}}
673
{{
674
    \\savenotes\\tcolorbox[{','.join(options)}]
675
    \\setcounter{{mpfootnote}}{{\\value{{footnote}}}}
676
    \\renewcommand\\thempfootnote{{\\arabic{{mpfootnote}}}}
677
}}
678
{{
679
    \\setcounter{{footnote}}{{\\value{{mpfootnote}}}}
680
    \\endtcolorbox\\spewnotes
681
}}
682
        """
683
684
685
def left_bar(environment: dict[str, Any]) -> str:
686
    """
687
    Generate a left bar.
688
689
    Arguments
690
    ---------
691
    environment
692
        The environment
693
694
    Returns
695
    -------
696
    str
697
        The left bar options
698
    """
699
    return bar(environment, "left", "west")
700
701
702
def right_bar(environment: dict[str, Any]) -> str:
703
    """
704
    Generate a right bar.
705
706
    Arguments
707
    ---------
708
    environment
709
        The environment
710
711
    Returns
712
    -------
713
    str
714
        The right bar options
715
    """
716
    return bar(environment, "right", "east")
717
718
719
# pylint: disable=blacklisted-name
720
def bar(environment: dict[str, Any], position: str, localization: str) -> str:
721
    """
722
    Generate a bar.
723
724
    Arguments
725
    ---------
726
    environment
727
        The environment
728
729
    position
730
        left or right
731
732
    localization
733
        east or west
734
735
    Returns
736
    -------
737
    str
738
        The bar options
739
    """
740
    return (
741
        f"{position}={environment['innermargin']:g}pt,borderline "
742
        f"{localization}={{{environment['linewidth']:g}pt}}"
743
        f"{{{environment['margin']:g}pt}}{{{environment['color']}}}"
744
    )
745
746
747
def finalize(doc: Doc) -> None:
748
    """
749
    Finalize the pandoc document.
750
751
    Arguments
752
    ---------
753
    doc
754
        The pandoc document
755
    """
756
    # load 'footnote' or 'footnotehyper' package
757
    if doc.format == "latex":
758
        doc.metadata["tables"] = MetaBool(True)
759
760
    # Add header-includes if necessary
761
    if "header-includes" not in doc.metadata:
762
        doc.metadata["header-includes"] = MetaList()
763
    # Convert header-includes to MetaList if necessary
764
    elif not isinstance(doc.metadata["header-includes"], MetaList):
765
        doc.metadata["header-includes"] = MetaList(doc.metadata["header-includes"])
766
767
    # Add useful LaTexPackage
768
    doc.metadata["header-includes"].append(
769
        MetaInlines(RawInline("\\usepackage{xcolor}", "tex"))
770
    )
771
772
    # Define x11 colors
773
    tex = [
774
        f"\\definecolor{{{name.lower()}}}{{HTML}}{{{color}}}"
775
        for name, color in doc.x11colors.items()
776
    ]
777
    doc.metadata["header-includes"].append(
778
        MetaInlines(RawInline("\n".join(tex), "tex"))
779
    )
780
    doc.metadata["header-includes"].append(
781
        MetaInlines(RawInline("\\usepackage[most]{tcolorbox}", "tex"))
782
    )
783
    doc.metadata["header-includes"].append(
784
        MetaInlines(
785
            RawInline(
786
                r"""
787
\usepackage{ifthen}
788
\provideboolean{admonitiontwoside}
789
\makeatletter%
790
\if@twoside%
791
\setboolean{admonitiontwoside}{true}
792
\else%
793
\setboolean{admonitiontwoside}{false}
794
\fi%
795
\makeatother%
796
""",
797
                "tex",
798
            )
799
        )
800
    )
801
    # Define specific environments
802
    for environment in doc.defined + doc.added:
803
        doc.metadata["header-includes"].append(
804
            MetaInlines(RawInline(new_environment(doc, environment), "tex"))
805
        )
806
807
808
def main(doc: Doc | None = None) -> Doc:
809
    """
810
    Convert the pandoc document.
811
812
    Arguments
813
    ---------
814
    doc
815
        The pandoc document
816
817
    Returns
818
    -------
819
    Doc
820
        The modified pandoc document
821
    """
822
    return run_filter(admonition, prepare=prepare, finalize=finalize, doc=doc)
823
824
825
if __name__ == "__main__":
826
    main()
827