Passed
Push — develop ( 5bfd4a...779a2f )
by Christophe
02:55 queued 13s
created

pandoc_latex_admonition._main.new_environment()   B

Complexity

Conditions 8

Size

Total Lines 47
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

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