Passed
Push — develop ( fe0b75...950657 )
by Christophe
02:43 queued 14s
created

_main.main()   A

Complexity

Conditions 1

Size

Total Lines 15
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 15
rs 10
c 0
b 0
f 0
cc 1
nop 1
1
#!/usr/bin/env python
2
3
"""
4
Pandoc filter for changing color in LaTeX.
5
"""
6
7
from typing import Any
8
9
from panflute import (
10
    Div,
11
    Doc,
12
    Element,
13
    MetaInlines,
14
    MetaList,
15
    RawBlock,
16
    RawInline,
17
    Span,
18
    debug,
19
    run_filter,
20
)
21
22
23
def x11colors() -> dict[str, str]:
24
    """
25
    Get the x1 colors.
26
27
    Returns
28
    -------
29
    dict[str, str]
30
        A dictionary of colornames -> colorcode.
31
    """
32
    # See https://www.w3.org/TR/css-color-3/#svg-color
33
    return {
34
        "aliceblue": "F0F8FF",
35
        "antiquewhite": "FAEBD7",
36
        "aqua": "00FFFF",
37
        "aquamarine": "7FFFD4",
38
        "azure": "F0FFFF",
39
        "beige": "F5F5DC",
40
        "bisque": "FFE4C4",
41
        "black": "000000",
42
        "blanchedalmond": "FFEBCD",
43
        "blue": "0000FF",
44
        "blueviolet": "8A2BE2",
45
        "brown": "A52A2A",
46
        "burlywood": "DEB887",
47
        "cadetblue": "5F9EA0",
48
        "chartreuse": "7FFF00",
49
        "chocolate": "D2691E",
50
        "coral": "FF7F50",
51
        "cornflowerblue": "6495ED",
52
        "cornsilk": "FFF8DC",
53
        "crimson": "DC143C",
54
        "cyan": "00FFFF",
55
        "darkblue": "00008B",
56
        "darkcyan": "008B8B",
57
        "darkgoldenrod": "B8860B",
58
        "darkgray": "A9A9A9",
59
        "darkgreen": "006400",
60
        "darkgrey": "A9A9A9",
61
        "darkkhaki": "BDB76B",
62
        "darkmagenta": "8B008B",
63
        "darkolivegreen": "556B2F",
64
        "darkorange": "FF8C00",
65
        "darkorchid": "9932CC",
66
        "darkred": "8B0000",
67
        "darksalmon": "E9967A",
68
        "darkseagreen": "8FBC8F",
69
        "darkslateblue": "483D8B",
70
        "darkslategray": "2F4F4F",
71
        "darkslategrey": "2F4F4F",
72
        "darkturquoise": "00CED1",
73
        "darkviolet": "9400D3",
74
        "deeppink": "FF1493",
75
        "deepskyblue": "00BFFF",
76
        "dimgray": "696969",
77
        "dimgrey": "696969",
78
        "dodgerblue": "1E90FF",
79
        "firebrick": "B22222",
80
        "floralwhite": "FFFAF0",
81
        "forestgreen": "228B22",
82
        "fuchsia": "FF00FF",
83
        "gainsboro": "DCDCDC",
84
        "ghostwhite": "F8F8FF",
85
        "gold": "FFD700",
86
        "goldenrod": "DAA520",
87
        "gray": "808080",
88
        "green": "008000",
89
        "greenyellow": "ADFF2F",
90
        "grey": "808080",
91
        "honeydew": "F0FFF0",
92
        "hotpink": "FF69B4",
93
        "indianred": "CD5C5C",
94
        "indigo": "4B0082",
95
        "ivory": "FFFFF0",
96
        "khaki": "F0E68C",
97
        "lavender": "E6E6FA",
98
        "lavenderblush": "FFF0F5",
99
        "lawngreen": "7CFC00",
100
        "lemonchiffon": "FFFACD",
101
        "lightblue": "ADD8E6",
102
        "lightcoral": "F08080",
103
        "lightcyan": "E0FFFF",
104
        "lightgoldenrodyellow": "FAFAD2",
105
        "lightgray": "D3D3D3",
106
        "lightgreen": "90EE90",
107
        "lightgrey": "D3D3D3",
108
        "lightpink": "FFB6C1",
109
        "lightsalmon": "FFA07A",
110
        "lightseagreen": "20B2AA",
111
        "lightskyblue": "87CEFA",
112
        "lightslategray": "778899",
113
        "lightslategrey": "778899",
114
        "lightsteelblue": "B0C4DE",
115
        "lightyellow": "FFFFE0",
116
        "lime": "00FF00",
117
        "limegreen": "32CD32",
118
        "linen": "FAF0E6",
119
        "magenta": "FF00FF",
120
        "maroon": "800000",
121
        "mediumaquamarine": "66CDAA",
122
        "mediumblue": "0000CD",
123
        "mediumorchid": "BA55D3",
124
        "mediumpurple": "9370DB",
125
        "mediumseagreen": "3CB371",
126
        "mediumslateblue": "7B68EE",
127
        "mediumspringgreen": "00FA9A",
128
        "mediumturquoise": "48D1CC",
129
        "mediumvioletred": "C71585",
130
        "midnightblue": "191970",
131
        "mintcream": "F5FFFA",
132
        "mistyrose": "FFE4E1",
133
        "moccasin": "FFE4B5",
134
        "navajowhite": "FFDEAD",
135
        "navy": "000080",
136
        "oldlace": "FDF5E6",
137
        "olive": "808000",
138
        "olivedrab": "6B8E23",
139
        "orange": "FFA500",
140
        "orangered": "FF4500",
141
        "orchid": "DA70D6",
142
        "palegoldenrod": "EEE8AA",
143
        "palegreen": "98FB98",
144
        "paleturquoise": "AFEEEE",
145
        "palevioletred": "DB7093",
146
        "papayawhip": "FFEFD5",
147
        "peachpuff": "FFDAB9",
148
        "peru": "CD853F",
149
        "pink": "FFC0CB",
150
        "plum": "DDA0DD",
151
        "powderblue": "B0E0E6",
152
        "purple": "800080",
153
        "red": "FF0000",
154
        "rosybrown": "BC8F8F",
155
        "royalblue": "4169E1",
156
        "saddlebrown": "8B4513",
157
        "salmon": "FA8072",
158
        "sandybrown": "F4A460",
159
        "seagreen": "2E8B57",
160
        "seashell": "FFF5EE",
161
        "sienna": "A0522D",
162
        "silver": "C0C0C0",
163
        "skyblue": "87CEEB",
164
        "slateblue": "6A5ACD",
165
        "slategray": "708090",
166
        "slategrey": "708090",
167
        "snow": "FFFAFA",
168
        "springgreen": "00FF7F",
169
        "steelblue": "4682B4",
170
        "tan": "D2B48C",
171
        "teal": "008080",
172
        "thistle": "D8BFD8",
173
        "tomato": "FF6347",
174
        "turquoise": "40E0D0",
175
        "violet": "EE82EE",
176
        "wheat": "F5DEB3",
177
        "white": "FFFFFF",
178
        "whitesmoke": "F5F5F5",
179
        "yellow": "FFFF00",
180
        "yellowgreen": "9ACD32",
181
    }
182
183
184
def color_code(color: str | bool) -> str:
185
    """
186
    Get the LaTeX color code.
187
188
    Arguments
189
    ---------
190
    color
191
        X11 color name
192
193
    Returns
194
    -------
195
    str
196
        The LaTeX color code.
197
    """
198
    if color:
199
        return f"\\color{{{color}}} "
200
    return "\\color{black} "
201
202
203
def bgcolor_code(color: str | bool) -> str | bool:
204
    """
205
    Get the LaTeX color code for the background.
206
207
    Arguments
208
    ---------
209
    color
210
        X11 color name
211
212
    Returns
213
    -------
214
    str | bool
215
        The LaTeX color code for the background.
216
    """
217
    if color:
218
        return f"\\sethlcolor{{{color}}}"
219
    return False
220
221
222
def get_correct_color(color: str | bool) -> str | bool:
223
    """
224
    Get a correct color name.
225
226
    Arguments
227
    ---------
228
    color
229
        Color name
230
231
    Returns
232
    -------
233
    str | bool
234
        A X11 color name or False.
235
    """
236
    if color in x11colors():
237
        return color
238
    if color:
239
        debug(
240
            f"[WARNING] pandoc-latex-color: {color}"
241
            f" is not a correct X11 color; using black"
242
        )
243
        return "black"
244
    return False
245
246
247
def add_latex(elem: Element, color: str | bool, bgcolor: str | bool) -> None:
248
    """
249
    Insert LaTeX code.
250
251
    Arguments
252
    ---------
253
    elem
254
        The pandoc element
255
    color
256
        The X11 color name
257
    bgcolor
258
        The X11 bgcolor name
259
    """
260
    # Is it a Span?
261
    if isinstance(elem, Span):
262
        if isinstance(bgcolor, str):
263
            elem.content.insert(0, RawInline(f"{bgcolor}\\hl{{", "tex"))
264
            elem.content.append(RawInline("}", "tex"))
265
266
        elem.content.insert(0, RawInline(color, "tex"))
267
268
    # Is it a Div?
269
    elif isinstance(elem, Div):
270
        if isinstance(bgcolor, str):
271
            elem.content.insert(0, RawBlock(f"{{{color}{bgcolor}\\hl{{", "tex"))
272
            elem.content.append(RawBlock("}", "tex"))
273
        else:
274
            elem.content.insert(0, RawBlock(f"{{{color}", "tex"))
275
            elem.content.append(RawBlock("}", "tex"))
276
277
278
def colorize(elem: Element, doc: Doc) -> None:
279
    """
280
    Colorize an element.
281
282
    Arguments
283
    ---------
284
    elem
285
        The pandoc element
286
    doc
287
        The pandoc document
288
    """
289
    # Is it in the right format and is it a Span, Div, Code or CodeBlock?
290
    if doc.format in ("latex", "beamer") and elem.tag in ("Span", "Div"):
291
        # Is there a latex-color attribute?
292
        if "latex-color" in elem.attributes or "latex-bgcolor" in elem.attributes:
293
            try:
294
                color = elem.attributes["latex-color"]
295
            except KeyError:
296
                color = "black"
297
298
            try:
299
                bgcolor = elem.attributes["latex-bgcolor"]
300
            except KeyError:
301
                bgcolor = False
302
303
            add_latex(
304
                elem,
305
                color_code(get_correct_color(color)),
306
                bgcolor_code(get_correct_color(bgcolor)),
307
            )
308
            return
309
310
        # Get the classes
311
        classes = set(elem.classes)
312
313
        # Loop on all color definition
314
        for definition in doc.defined:
315
            # Are the classes correct?
316
            if classes >= definition["classes"]:
317
                add_latex(elem, definition["color"], definition["bgcolor"])
318
                return
319
320
321
def prepare(doc: Doc) -> None:
322
    """
323
    Prepare the document.
324
325
    Arguments
326
    ---------
327
    doc
328
        The pandoc document
329
    """
330
    # Prepare the definitions
331
    doc.defined = []
332
333
    # Get the meta data
334
    meta = doc.get_metadata("pandoc-latex-color")
335
336
    if isinstance(meta, list):
337
        # Loop on all definitions
338
        for definition in meta:
339
            # Verify the definition
340
            if (
341
                isinstance(definition, dict)
342
                and "classes" in definition
343
                and isinstance(definition["classes"], list)
344
            ):
345
                add_definition(doc.defined, definition)
346
347
348
def add_definition(defined: list[dict[str, Any]], definition: dict[str, Any]) -> None:
349
    """
350
    Add a definition.
351
352
    Arguments
353
    ---------
354
    defined
355
        A list of definition
356
    definition
357
        A new definition
358
    """
359
    # Get the classes
360
    classes = definition["classes"]
361
362
    # Get the color
363
    if "color" in definition:
364
        color = get_correct_color(definition["color"])
365
    else:
366
        color = "black"
367
368
    # Get the bgcolor
369
    bgcolor: str | bool = False
370
    if "bgcolor" in definition:
371
        bgcolor = get_correct_color(definition["bgcolor"])
372
    else:
373
        bgcolor = False
374
375
    # Add a definition
376
    defined.append(
377
        {
378
            "classes": set(classes),
379
            "color": color_code(color),
380
            "bgcolor": bgcolor_code(bgcolor),
381
        }
382
    )
383
384
385
def finalize(doc: Doc) -> None:
386
    """
387
    Finalize the document.
388
389
    Arguments
390
    ---------
391
    doc
392
        The pandoc document
393
    """
394
    # Add header-includes if necessary
395
    if "header-includes" not in doc.metadata:
396
        doc.metadata["header-includes"] = MetaList()
397
    # Convert header-includes to MetaList if necessary
398
    elif not isinstance(doc.metadata["header-includes"], MetaList):
399
        doc.metadata["header-includes"] = MetaList(doc.metadata["header-includes"])
400
401
    # Add useful LaTexPackage
402
    doc.metadata["header-includes"].append(
403
        MetaInlines(RawInline("\\usepackage{xcolor}", "tex"))
404
    )
405
    doc.metadata["header-includes"].append(
406
        MetaInlines(RawInline("\\usepackage{soulutf8,color}", "tex"))
407
    )
408
    doc.metadata["header-includes"].append(
409
        MetaInlines(RawInline("\\soulregister\\cite7", "tex"))
410
    )
411
    doc.metadata["header-includes"].append(
412
        MetaInlines(RawInline("\\soulregister\\ref7", "tex"))
413
    )
414
    doc.metadata["header-includes"].append(
415
        MetaInlines(RawInline("\\soulregister\\pageref7", "tex"))
416
    )
417
418
    for color, value in x11colors().items():
419
        doc.metadata["header-includes"].append(
420
            MetaInlines(
421
                RawInline(f"\\definecolor{{{color}}}{{HTML}}{{{value}}}", "tex")
422
            )
423
        )
424
425
    if doc.format == "beamer":
426
        special_beamer = [
427
            "\\makeatletter",
428
            "\\let\\HL\\hl",
429
            "\\renewcommand\\hl{%",
430
            "\\let\\set@color\\beamerorig@set@color",
431
            "\\let\\reset@color\\beamerorig@reset@color",
432
            "\\HL}",
433
            "\\makeatother",
434
        ]
435
        for line in special_beamer:
436
            doc.metadata["header-includes"].append(MetaInlines(RawInline(line, "tex")))
437
438
439
def main(doc: Doc | None = None) -> Doc:
440
    """
441
    Convert the pandoc document.
442
443
    Arguments
444
    ---------
445
    doc
446
        The pandoc document
447
448
    Returns
449
    -------
450
    Doc
451
        The modified pandoc document
452
    """
453
    return run_filter(colorize, prepare=prepare, finalize=finalize, doc=doc)
454
455
456
if __name__ == "__main__":
457
    main()
458