Passed
Push — develop ( 28f16a...7191f9 )
by Christophe
02:23
created

pandoc_numbering.meta_listing()   C

Complexity

Conditions 11

Size

Total Lines 49
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
eloc 28
nop 3
dl 0
loc 49
rs 5.4
c 0
b 0
f 0

How to fix   Complexity   

Complexity

Complex classes like pandoc_numbering.meta_listing() 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
# pylint: disable=too-many-lines
4
5
"""Pandoc filter to number all kinds of things."""
6
7
import copy
8
import re
9
import unicodedata
10
from functools import partial
11
from textwrap import dedent
12
13
from panflute import (
14
    BlockQuote,
15
    BulletList,
16
    Citation,
17
    Cite,
18
    CodeBlock,
19
    Definition,
20
    DefinitionItem,
21
    DefinitionList,
22
    Div,
23
    Emph,
24
    Header,
25
    HorizontalRule,
26
    Image,
27
    LineBlock,
28
    LineBreak,
29
    LineItem,
30
    Link,
31
    ListItem,
32
    MetaBool,
33
    MetaInlines,
34
    MetaList,
35
    MetaMap,
36
    MetaString,
37
    Note,
38
    Para,
39
    Plain,
40
    RawBlock,
41
    RawInline,
42
    SoftBreak,
43
    Space,
44
    Span,
45
    Str,
46
    Strong,
47
    Table,
48
    TableCell,
49
    TableRow,
50
    convert_text,
51
    debug,
52
    run_filters,
53
    stringify,
54
)
55
56
57
# pylint: disable=bad-option-value,useless-object-inheritance
58
class Numbered:
59
    """
60
    Numbered elements.
61
    """
62
63
    # pylint: disable=too-many-instance-attributes
64
    __slots__ = [
65
        "_elem",
66
        "_doc",
67
        "_match",
68
        "_tag",
69
        "_entry",
70
        "_link",
71
        "_caption",
72
        "_title",
73
        "_description",
74
        "_category",
75
        "_basic_category",
76
        "_first_section_level",
77
        "_last_section_level",
78
        "_leading",
79
        "_number",
80
        "_global_number",
81
        "_section_number",
82
        "_local_number",
83
        "_section_alias",
84
        "_alias",
85
    ]
86
87
    @property
88
    def tag(self):
89
        """
90
        Get the tag property.
91
92
        Returns
93
        -------
94
            The tag property.
95
        """
96
        return self._tag
97
98
    @property
99
    def entry(self):
100
        """
101
        Get the entry property.
102
103
        Returns
104
        -------
105
            The entry property.
106
        """
107
        return self._entry
108
109
    @property
110
    def link(self):
111
        """
112
        Get the link property.
113
114
        Returns
115
        -------
116
            The link property.
117
        """
118
        return self._link
119
120
    @property
121
    def title(self):
122
        """
123
        Get the title property.
124
125
        Returns
126
        -------
127
            The title property.
128
        """
129
        return self._title
130
131
    @property
132
    def description(self):
133
        """
134
        Get the description property.
135
136
        Returns
137
        -------
138
            The description property.
139
        """
140
        return self._description
141
142
    @property
143
    def global_number(self):
144
        """
145
        Get the global_number property.
146
147
        Returns
148
        -------
149
            The global_number property.
150
        """
151
        return self._global_number
152
153
    @property
154
    def section_number(self):
155
        """
156
        Get the section_number property.
157
158
        Returns
159
        -------
160
            The section_number property.
161
        """
162
        return self._section_number
163
164
    @property
165
    def section_alias(self):
166
        """
167
        Get the section_alias property.
168
169
        Returns
170
        -------
171
            The section_alias property.
172
        """
173
        return self._section_alias
174
175
    @property
176
    def alias(self):
177
        """
178
        Get the alias property.
179
180
        Returns
181
        -------
182
            The alias property.
183
        """
184
        return self._alias
185
186
    @property
187
    def local_number(self):
188
        """
189
        Get the local_number property.
190
191
        Returns
192
        -------
193
            The local_number property.
194
        """
195
        return self._local_number
196
197
    @property
198
    def category(self):
199
        """
200
        Get the category property.
201
202
        Returns
203
        -------
204
            The category property.
205
        """
206
        return self._category
207
208
    @property
209
    def caption(self):
210
        """
211
        Get the caption property.
212
213
        Returns
214
        -------
215
            The caption property.
216
        """
217
        return self._caption
218
219
    number_regex = "#((?P<prefix>[a-zA-Z][\\w.-]*):)?(?P<name>[a-zA-Z][\\w:.-]*)?"
220
    _regex = "(?P<header>(?P<hidden>(-\\.)*)(\\+\\.)*)"
221
    header_regex = "^" + _regex + "$"
222
    marker_regex = "^" + _regex + number_regex + "$"
223
    double_sharp_regex = "^" + _regex + "#" + number_regex + "$"
224
225
    @staticmethod
226
    def _remove_accents(string):
227
        nfkd_form = unicodedata.normalize("NFKD", string)
228
        # pylint: disable=redundant-u-string-prefix
229
        return "".join([c for c in nfkd_form if not unicodedata.combining(c)])
230
231
    @staticmethod
232
    def _identifier(string):
233
        # replace invalid characters by dash
234
        string = re.sub(
235
            "[^0-9a-zA-Z_-]+", "-", Numbered._remove_accents(string.lower())
236
        )
237
238
        # Remove leading digits
239
        return re.sub("^[^a-zA-Z]+", "", string)
240
241
    def __init__(self, elem, doc):
242
        """
243
        Initialise an instance.
244
245
        Arguments
246
        ---------
247
        elem
248
            An element.
249
        doc
250
            The document.
251
        """
252
        self._elem = elem
253
        self._doc = doc
254
        self._tag = None
255
        self._entry = Span(classes=["pandoc-numbering-entry"])
256
        self._link = Span(classes=["pandoc-numbering-link"])
257
        self._caption = None
258
        self._title = None
259
        self._description = None
260
        self._category = None
261
        self._basic_category = None
262
        self._first_section_level = None
263
        self._last_section_level = None
264
        self._leading = None
265
        self._number = None
266
        self._global_number = None
267
        self._section_number = None
268
        self._local_number = None
269
        self._section_alias = None
270
        self._alias = None
271
272
        if self._get_content() and isinstance(self._get_content()[-1], Str):
273
            self._match = re.match(Numbered.marker_regex, self._get_content()[-1].text)
274
            if self._match:
275
                self._replace_marker()
276
            elif re.match(Numbered.double_sharp_regex, self._get_content()[-1].text):
277
                self._replace_double_sharp()
278
279
    def _set_content(self, content):
280
        if isinstance(self._elem, Para):
281
            self._elem.content = content
282
        elif isinstance(self._elem, DefinitionItem):
283
            self._elem.term = content
284
285
    def _get_content(self):
286
        if isinstance(self._elem, Para):
287
            return self._elem.content
288
        if isinstance(self._elem, DefinitionItem):
289
            return self._elem.term
290
        return None
291
292
    def _replace_double_sharp(self):
293
        self._get_content()[-1].text = self._get_content()[-1].text.replace(
294
            "##", "#", 1
295
        )
296
297
    def _replace_marker(self):
298
        self._compute_title()
299
        self._compute_description()
300
        self._compute_basic_category()
301
        self._compute_levels()
302
        self._compute_section_number()
303
        self._compute_section_alias()
304
        self._compute_leading()
305
        self._compute_category()
306
        self._compute_number()
307
        self._compute_tag()
308
        self._compute_alias()
309
        self._compute_local_number()
310
        self._compute_global_number()
311
        self._compute_data()
312
313
    def _compute_title(self):
314
        self._title = []
315
        if (
316
            isinstance(self._get_content()[-3], Str)
317
            and self._get_content()[-3].text[-1:] == ")"
318
        ):
319
            for i, item in enumerate(self._get_content()):
320
                if isinstance(item, Str) and item.text[0] == "(":
321
                    self._title = self._get_content()[i:-2]
322
                    # Detach from original parent
323
                    self._title.parent = None
324
                    self._title[0].text = self._title[0].text[1:]
325
                    self._title[-1].text = self._title[-1].text[:-1]
326
                    del self._get_content()[i - 1 : -2]
327
                    break
328
        self._title = list(self._title)
329
330
    def _compute_description(self):
331
        self._description = self._get_content()[:-2]
332
        # Detach from original parent
333
        self._description.parent = None
334
        self._description = list(self._description)
335
336
    def _compute_basic_category(self):
337
        if self._match.group("prefix") is None:
338
            self._basic_category = Numbered._identifier(
339
                "".join(map(stringify, self._description))
340
            )
341
        else:
342
            self._basic_category = self._match.group("prefix")
343
        if self._basic_category not in self._doc.defined:
344
            define(self._basic_category, self._doc)
345
346
    def _compute_levels(self):
347
        # Compute the first and last section level values
348
        self._first_section_level = len(self._match.group("hidden")) // 2
349
        self._last_section_level = len(self._match.group("header")) // 2
350
351
        # Get the default first and last section level
352
        if self._first_section_level == self._last_section_level == 0:
353
            self._first_section_level = self._doc.defined[self._basic_category][
354
                "first-section-level"
355
            ]
356
            self._last_section_level = self._doc.defined[self._basic_category][
357
                "last-section-level"
358
            ]
359
360
    def _compute_section_number(self):
361
        self._section_number = ".".join(
362
            map(str, self._doc.headers[: self._last_section_level])
363
        )
364
365
    def _compute_section_alias(self):
366
        strings = list(map(str, self._doc.aliases[: self._last_section_level]))
367
        for index, string in enumerate(strings):
368
            if string == "":
369
                strings[index] = "0"
370
        self._section_alias = ".".join(strings)
371
372
    def _compute_leading(self):
373
        # Compute the leading (composed of the section numbering and a dot)
374
        if self._last_section_level != 0:
375
            self._leading = self._section_number + "."
376
        else:
377
            self._leading = ""
378
379
    def _compute_category(self):
380
        self._category = self._basic_category + ":" + self._leading
381
382
        # Is it a new category?
383
        if self._category not in self._doc.count:
384
            self._doc.count[self._category] = 0
385
386
        self._doc.count[self._category] = self._doc.count[self._category] + 1
387
388
    def _compute_number(self):
389
        self._number = str(self._doc.count[self._category])
390
391
    def _compute_tag(self):
392
        # Determine the final tag
393
        if self._match.group("name") is None:
394
            self._tag = self._category + self._number
395
        else:
396
            self._tag = self._basic_category + ":" + self._match.group("name")
397
398
        # Compute collections
399
        if self._basic_category not in self._doc.collections:
400
            self._doc.collections[self._basic_category] = []
401
402
        self._doc.collections[self._basic_category].append(self._tag)
403
404
    def _compute_alias(self):
405
        # Determine the final alias
406
        if not self._title:
407
            if self._section_alias:
408
                self._alias = (
409
                    self._basic_category
410
                    + ":"
411
                    + self._section_alias
412
                    + "."
413
                    + self._number
414
                )
415
            else:
416
                self._alias = self._basic_category + ":" + self._number
417
        else:
418
            if self._section_alias:
419
                self._alias = (
420
                    self._basic_category
421
                    + ":"
422
                    + self._section_alias
423
                    + "."
424
                    + Numbered._identifier(stringify(Span(*self._title)))
425
                )
426
            else:
427
                self._alias = (
428
                    self._basic_category
429
                    + ":"
430
                    + Numbered._identifier(stringify(Span(*self._title)))
431
                )
432
433
    def _compute_local_number(self):
434
        # Replace the '-.-.+.+...#' by the category count (omitting the hidden part)
435
        self._local_number = ".".join(
436
            map(
437
                str,
438
                self._doc.headers[self._first_section_level : self._last_section_level]
439
                + [self._number],
440
            )
441
        )
442
443
    def _compute_global_number(self):
444
        # Compute the global number
445
        if self._section_number:
446
            self._global_number = self._section_number + "." + self._number
447
        else:
448
            self._global_number = self._number
449
450
    def _compute_data(self):
451
        # pylint: disable=too-many-statements,no-member
452
        classes = self._doc.defined[self._basic_category]["classes"]
453
        if self._alias == self._tag:
454
            self._set_content(
455
                [
456
                    Span(),
457
                    Span(
458
                        identifier=self._tag,
459
                        classes=["pandoc-numbering-text"] + classes,
460
                    ),
461
                ]
462
            )
463
        else:
464
            self._set_content(
465
                [
466
                    Span(identifier=self._alias),
467
                    Span(
468
                        identifier=self._tag,
469
                        classes=["pandoc-numbering-text"] + classes,
470
                    ),
471
                ]
472
            )
473
        self._link.classes = self._link.classes + classes
474
        self._entry.classes = self._entry.classes + classes
475
476
        # Prepare the final data
477
        if self._title:
478
            self._get_content()[1].content = copy.deepcopy(
479
                self._doc.defined[self._basic_category]["format-text-title"]
480
            )
481
            self._link.content = copy.deepcopy(
482
                self._doc.defined[self._basic_category]["format-link-title"]
483
            )
484
            self._entry.content = copy.deepcopy(
485
                self._doc.defined[self._basic_category]["format-entry-title"]
486
            )
487
            self._caption = self._doc.defined[self._basic_category][
488
                "format-caption-title"
489
            ]
490
        else:
491
            self._get_content()[1].content = copy.deepcopy(
492
                self._doc.defined[self._basic_category]["format-text-classic"]
493
            )
494
            self._link.content = copy.deepcopy(
495
                self._doc.defined[self._basic_category]["format-link-classic"]
496
            )
497
            self._entry.content = copy.deepcopy(
498
                self._doc.defined[self._basic_category]["format-entry-classic"]
499
            )
500
            self._caption = self._doc.defined[self._basic_category][
501
                "format-caption-classic"
502
            ]
503
504
        # Compute caption (delay replacing %c at the end)
505
        title = stringify(Span(*self._title))
506
        description = stringify(Span(*self._description))
507
        self._caption = self._caption.replace("%t", title.lower())
508
        self._caption = self._caption.replace("%T", title)
509
        self._caption = self._caption.replace("%d", description.lower())
510
        self._caption = self._caption.replace("%D", description)
511
        self._caption = self._caption.replace("%s", self._section_number)
512
        self._caption = self._caption.replace("%g", self._global_number)
513
        self._caption = self._caption.replace("%n", self._local_number)
514
        self._caption = self._caption.replace("#", self._local_number)
515
        if self._doc.format in {"tex", "latex"}:
516
            self._caption = self._caption.replace("%p", "\\pageref{" + self._tag + "}")
517
518
        # Compute content
519
        if isinstance(self._elem, DefinitionItem):
520
            replace_description(Plain(*self._elem.term), self._description)
521
            replace_title(Plain(*self._elem.term), self._title)
522
            replace_global_number(Plain(*self._elem.term), self._global_number)
523
            replace_section_number(Plain(*self._elem.term), self._section_number)
524
            replace_local_number(Plain(*self._elem.term), self._local_number)
525
        else:
526
            replace_description(self._elem, self._description)
527
            replace_title(self._elem, self._title)
528
            replace_global_number(self._elem, self._global_number)
529
            replace_section_number(self._elem, self._section_number)
530
            replace_local_number(self._elem, self._local_number)
531
532
        # Compute link
533
        replace_description(self._link, self._description)
534
        replace_title(self._link, self._title)
535
        replace_global_number(self._link, self._global_number)
536
        replace_section_number(self._link, self._section_number)
537
        replace_local_number(self._link, self._local_number)
538
        if self._doc.format in {"tex", "latex"}:
539
            replace_page_number(self._link, self._tag)
540
541
        # Compute entry
542
        replace_description(self._entry, self._description)
543
        replace_title(self._entry, self._title)
544
        replace_global_number(self._entry, self._global_number)
545
        replace_section_number(self._entry, self._section_number)
546
        replace_local_number(self._entry, self._local_number)
547
548
        # Finalize the content
549
        if self._doc.format in {"tex", "latex"}:
550
            latex_category = re.sub("[^a-z]+", "", self._basic_category)
551
            latex = (
552
                "\\phantomsection"
553
                f"\\addcontentsline{{{latex_category}}}{{{latex_category}}}"
554
                f"{{\\protect\\numberline {{{self._leading + self._number}}}"
555
                f"{{\\ignorespaces {to_latex(self._entry)}"
556
                "}}"
557
            )
558
            self._get_content().insert(0, RawInline(latex, "tex"))
559
560
561
def replace_description(where, description):
562
    """
563
    Replace description in where.
564
565
    Arguments
566
    ---------
567
    where
568
        where to replace
569
    description
570
        replace %D and %d by description
571
    """
572
    where.walk(partial(replacing, search="%D", replace=copy.deepcopy(description)))
573
    where.walk(
574
        partial(
575
            replacing,
576
            search="%d",
577
            replace=[item.walk(lowering) for item in copy.deepcopy(description)],
578
        )
579
    )
580
581
582
def replace_title(where, title):
583
    """
584
    Replace title in where.
585
586
    Arguments
587
    ---------
588
    where
589
        where to replace
590
    title
591
        replace %T and %t by title
592
    """
593
    where.walk(partial(replacing, search="%T", replace=copy.deepcopy(title)))
594
    where.walk(
595
        partial(
596
            replacing,
597
            search="%t",
598
            replace=[item.walk(lowering) for item in copy.deepcopy(title)],
599
        )
600
    )
601
602
603
def replace_section_number(where, section_number):
604
    """
605
    Replace section number in where.
606
607
    Arguments
608
    ---------
609
    where
610
        where to replace
611
    section_number
612
        replace %s by section_number
613
    """
614
    where.walk(partial(replacing, search="%s", replace=[Str(section_number)]))
615
616
617
def replace_global_number(where, global_number):
618
    """
619
    Replace global number in where.
620
621
    Arguments
622
    ---------
623
    where
624
        where to replace
625
    global_number
626
        replace %g by global_number
627
    """
628
    where.walk(partial(replacing, search="%g", replace=[Str(global_number)]))
629
630
631
def replace_local_number(where, local_number):
632
    """
633
    Replace local number in where.
634
635
    Arguments
636
    ---------
637
    where
638
        where to replace
639
    local_number
640
        replace %n and # by local_number
641
    """
642
    where.walk(partial(replacing, search="%n", replace=[Str(local_number)]))
643
    where.walk(partial(replacing, search="#", replace=[Str(local_number)]))
644
645
646
def replace_page_number(where, tag):
647
    """
648
    Replace page number in where.
649
650
    Arguments
651
    ---------
652
    where
653
        where to replace
654
    tag
655
        replace %p by tag
656
    """
657
    where.walk(
658
        partial(
659
            replacing, search="%p", replace=[RawInline("\\pageref{" + tag + "}", "tex")]
660
        )
661
    )
662
663
664
def replace_count(where, count):
665
    """
666
    Replace count in where.
667
668
    Arguments
669
    ---------
670
    where
671
        where to replace
672
    count
673
        replace %c by count
674
    """
675
    where.walk(partial(replacing, search="%c", replace=[Str(count)]))
676
677
678
def remove_useless_latex(elem, _):
679
    """
680
    Clean up LaTeX element for entries.
681
682
    Arguments
683
    ---------
684
    elem
685
        elem to scan
686
687
    Returns
688
    -------
689
        []: if elem is an instance to remove
690
        None: otherwise
691
    """
692
    if isinstance(
693
        elem,
694
        (
695
            BlockQuote,
696
            BulletList,
697
            Citation,
698
            Cite,
699
            CodeBlock,
700
            Definition,
701
            DefinitionItem,
702
            DefinitionList,
703
            Div,
704
            Header,
705
            HorizontalRule,
706
            Image,
707
            LineBlock,
708
            LineBreak,
709
            LineItem,
710
            ListItem,
711
            Note,
712
            Para,
713
            RawBlock,
714
            RawInline,
715
            SoftBreak,
716
            Table,
717
            TableCell,
718
            TableRow,
719
        ),
720
    ):
721
        return []
722
    return None
723
724
725
def to_latex(elem):
726
    """
727
    Convert element to LaTeX.
728
729
    Arguments
730
    ---------
731
    elem
732
        elem to convert
733
734
    Returns
735
    -------
736
        LaTex string
737
    """
738
    return convert_text(
739
        run_filters([remove_useless_latex], doc=Plain(elem)),
740
        input_format="panflute",
741
        output_format="latex",
742
        extra_args=["--no-highlight"],
743
    )
744
745
746
def define(category, doc):
747
    """
748
    Define a category in document.
749
750
    Arguments
751
    ---------
752
    category
753
        category to define
754
    doc
755
        pandoc document
756
    """
757
    # pylint: disable=line-too-long
758
    doc.defined[category] = {
759
        "first-section-level": 0,
760
        "last-section-level": 0,
761
        "format-text-classic": [Strong(Str("%D"), Space(), Str("%n"))],
762
        "format-text-title": [
763
            Strong(Str("%D"), Space(), Str("%n")),
764
            Space(),
765
            Emph(Str("(%T)")),
766
        ],
767
        "format-link-classic": [Str("%D"), Space(), Str("%n")],
768
        "format-link-title": [Str("%D"), Space(), Str("%n"), Space(), Str("(%T)")],
769
        "format-caption-classic": "%D %n",
770
        "format-caption-title": "%D %n (%T)",
771
        "format-entry-title": [Str("%T")],
772
        "classes": [category],
773
        "cite-shortcut": True,
774
        "listing-title": None,
775
        "listing-unnumbered": True,
776
        "listing-unlisted": True,
777
        "listing-identifier": True,
778
        "entry-tab": 1.5,
779
        "entry-space": 2.3,
780
    }
781
    if doc.format == "latex":
782
        doc.defined[category]["format-entry-classic"] = [Str("%D")]
783
        doc.defined[category]["entry-tab"] = 1.5
784
        doc.defined[category]["entry-space"] = 2.3
785
    else:
786
        doc.defined[category]["format-entry-classic"] = [Str("%D"), Space(), Str("%g")]
787
788
789
def lowering(elem, _):
790
    """
791
    Lower element.
792
793
    Arguments
794
    ---------
795
    elem
796
        element to lower
797
    """
798
    if isinstance(elem, Str):
799
        elem.text = elem.text.lower()
800
801
802
def replacing(elem, _, search=None, replace=None):
803
    """
804
    Replace an element.
805
806
    Arguments
807
    ---------
808
    elem
809
        element to scan
810
    search
811
        string to search
812
    replace
813
        string to replace
814
815
    Returns
816
    -------
817
        The modified elements.
818
    """
819
    if isinstance(elem, Str):
820
        search_splitted = elem.text.split(search)
821
        if len(search_splitted) > 1:
822
            text = []
823
824
            if search_splitted[0] != "":
825
                text.append(Str(search_splitted[0]))
826
827
            for string in search_splitted[1:]:
828
                text.extend(replace)
829
                if string != "":
830
                    text.append(Str(string))
831
832
            return text
833
834
    return [elem]
835
836
837
def numbering(elem, doc):
838
    """
839
    Add the numbering of an element.
840
841
    Arguments
842
    ---------
843
    elem
844
        element to number
845
    doc
846
        pandoc document
847
    """
848
    if isinstance(elem, Header):
849
        update_header_numbers(elem, doc)
850
        update_header_aliases(elem, doc)
851
    elif isinstance(elem, (Para, DefinitionItem)):
852
        numbered = Numbered(elem, doc)
853
        if numbered.tag is not None:
854
            doc.information[numbered.tag] = numbered
855
856
857
def referencing(elem, doc):
858
    """
859
    Add a reference for an element.
860
861
    Arguments
862
    ---------
863
    elem
864
        element to reference
865
    doc
866
        pandoc document
867
868
    Returns
869
    -------
870
        A Link or None
871
    """
872
    if isinstance(elem, Link):
873
        return referencing_link(elem, doc)
874
    if isinstance(elem, Cite):
875
        return referencing_cite(elem, doc)
876
    if isinstance(elem, Span) and elem.identifier in doc.information:
877
        replace_count(elem, str(doc.count[doc.information[elem.identifier].category]))
878
    return None
879
880
881
def referencing_link(elem, doc):
882
    """
883
    Add a eference link.
884
885
    Arguments
886
    ---------
887
    elem
888
        element to reference
889
    doc
890
        pandoc document
891
    """
892
    match = re.match("^#(?P<tag>([a-zA-Z][\\w:.-]*))$", elem.url)
893
    if match:
894
        tag = match.group("tag")
895
        if tag in doc.information:
896
            replace_title(elem, doc.information[tag].title)
897
            replace_description(elem, doc.information[tag].description)
898
            replace_global_number(elem, doc.information[tag].global_number)
899
            replace_section_number(elem, doc.information[tag].section_number)
900
            replace_local_number(elem, doc.information[tag].local_number)
901
            replace_count(elem, str(doc.count[doc.information[tag].category]))
902
            if doc.format in {"tex", "latex"}:
903
                replace_page_number(elem, tag)
904
905
            title = stringify(Span(*doc.information[tag].title))
906
            description = stringify(Span(*doc.information[tag].description))
907
            elem.title = elem.title.replace("%t", title.lower())
908
            elem.title = elem.title.replace("%T", title)
909
            elem.title = elem.title.replace("%d", description.lower())
910
            elem.title = elem.title.replace("%D", description)
911
            elem.title = elem.title.replace("%s", doc.information[tag].section_number)
912
            elem.title = elem.title.replace("%g", doc.information[tag].global_number)
913
            elem.title = elem.title.replace("%n", doc.information[tag].local_number)
914
            elem.title = elem.title.replace("#", doc.information[tag].local_number)
915
            elem.title = elem.title.replace(
916
                "%c", str(doc.count[doc.information[tag].category])
917
            )
918
            if doc.format in {"tex", "latex"}:
919
                elem.title = elem.title.replace("%p", "\\pageref{" + tag + "}")
920
921
922
def referencing_cite(elem, doc):
923
    """
924
    Cite reference.
925
926
    Arguments
927
    ---------
928
    elem
929
        element to reference
930
    doc
931
        pandoc document
932
933
    Returns
934
    -------
935
        A Link or None
936
    """
937
    if len(elem.content) == 1 and isinstance(elem.content[0], Str):
938
        match = re.match(
939
            "^(@(?P<tag>(?P<category>[a-zA-Z][\\w.-]*):"
940
            "(([a-zA-Z][\\w.-]*)|(\\d*(\\.\\d*)*))))$",
941
            elem.content[0].text,
942
        )
943
        if match:
944
            category = match.group("category")
945
            if category in doc.defined and doc.defined[category]["cite-shortcut"]:
946
                # Deal with @prefix:name shortcut
947
                tag = match.group("tag")
948
                if tag in doc.information:
949
                    ret = Link(
950
                        doc.information[tag].link,
951
                        url="#" + tag,
952
                        title=doc.information[tag].caption.replace(
953
                            "%c", str(doc.count[doc.information[tag].category])
954
                        ),
955
                    )
956
                    replace_count(ret, str(doc.count[doc.information[tag].category]))
957
                    return ret
958
    return None
959
960
961
def update_header_numbers(elem, doc):
962
    """
963
    Update header numbers.
964
965
    Arguments
966
    ---------
967
    elem
968
        element to update
969
    doc
970
        pandoc document
971
    """
972
    if "unnumbered" not in elem.classes:
973
        doc.headers[elem.level - 1] = doc.headers[elem.level - 1] + 1
974
        for index in range(elem.level, 6):
975
            doc.headers[index] = 0
976
977
978
def update_header_aliases(elem, doc):
979
    """
980
    Update header aliases.
981
982
    Arguments
983
    ---------
984
    elem
985
        element to update
986
    doc
987
        pandoc document
988
    """
989
    doc.aliases[elem.level - 1] = elem.identifier
990
    for index in range(elem.level, 6):
991
        doc.aliases[index] = ""
992
993
994
def prepare(doc):
995
    """
996
    Prepare document.
997
998
    Arguments
999
    ---------
1000
    doc
1001
        pandoc document
1002
    """
1003
    doc.headers = [0, 0, 0, 0, 0, 0]
1004
    doc.aliases = ["", "", "", "", "", ""]
1005
    doc.information = {}
1006
    doc.defined = {}
1007
1008
    if "pandoc-numbering" in doc.metadata.content and isinstance(
1009
        doc.metadata.content["pandoc-numbering"], MetaMap
1010
    ):
1011
        for category, definition in doc.metadata.content[
1012
            "pandoc-numbering"
1013
        ].content.items():
1014
            if isinstance(definition, MetaMap):
1015
                add_definition(category, definition, doc)
1016
1017
    doc.count = {}
1018
    doc.collections = {}
1019
1020
1021
def add_definition(category, definition, doc):
1022
    """
1023
    Add definition for a category.
1024
1025
    Arguments
1026
    ---------
1027
    category
1028
        The category
1029
    definition
1030
        The definition
1031
    doc
1032
        The pandoc document
1033
    """
1034
    # Create the category with options by default
1035
    define(category, doc)
1036
1037
    # Detect general options
1038
    if "general" in definition:
1039
        meta_cite(category, definition["general"], doc.defined)
1040
        meta_listing(category, definition["general"], doc.defined)
1041
        meta_levels(category, definition["general"], doc.defined)
1042
        meta_classes(category, definition["general"], doc.defined)
1043
1044
    # Detect LaTeX options
1045
    if doc.format in {"tex", "latex"}:
1046
        if "latex" in definition:
1047
            meta_format_text(category, definition["latex"], doc.defined)
1048
            meta_format_link(category, definition["latex"], doc.defined)
1049
            meta_format_caption(category, definition["latex"], doc.defined)
1050
            meta_format_entry(category, definition["latex"], doc.defined)
1051
            meta_entry_tab(category, definition["latex"], doc.defined)
1052
            meta_entry_space(category, definition["latex"], doc.defined)
1053
    # Detect standard options
1054
    else:
1055
        if "standard" in definition:
1056
            meta_format_text(category, definition["standard"], doc.defined)
1057
            meta_format_link(category, definition["standard"], doc.defined)
1058
            meta_format_caption(category, definition["standard"], doc.defined)
1059
            meta_format_entry(category, definition["standard"], doc.defined)
1060
1061
1062
def meta_cite(category, definition, defined):
1063
    """
1064
    Compute cite for a category.
1065
1066
    Arguments
1067
    ---------
1068
    category
1069
        The category
1070
    definition
1071
        The definition
1072
    defined
1073
        The defined parameter
1074
    """
1075
    if "cite-shortcut" in definition:
1076
        if isinstance(definition["cite-shortcut"], MetaBool):
1077
            defined[category]["cite-shortcut"] = definition["cite-shortcut"].boolean
1078
        else:
1079
            debug(
1080
                "[WARNING] pandoc-numbering: cite-shortcut is not correct for category "
1081
                + category
1082
            )
1083
1084
1085
# pylint:disable=too-many-branches
1086
def meta_listing(category, definition, defined):
1087
    """
1088
    Compute listing for a category.
1089
1090
    Arguments
1091
    ---------
1092
    category
1093
        The category
1094
    definition
1095
        The definition
1096
    defined
1097
        The defined parameter
1098
    """
1099
    if "listing-title" in definition:
1100
        if isinstance(definition["listing-title"], MetaInlines):
1101
            defined[category]["listing-title"] = definition["listing-title"].content
1102
            # Detach from original parent
1103
            defined[category]["listing-title"].parent = None
1104
        else:
1105
            debug(
1106
                "[WARNING] pandoc-numbering: listing-title is not correct for category "
1107
                + category
1108
            )
1109
    for key in ("listing-unnumbered", "listing-unlisted"):
1110
        if key in definition:
1111
            if isinstance(definition[key], MetaBool):
1112
                defined[category][key] = definition[key].boolean
1113
            else:
1114
                debug(
1115
                    f"[WARNING] pandoc-numbering: "
1116
                    f"{key} is not correct for category {category}"
1117
                )
1118
    if "listing-identifier" in definition:
1119
        if isinstance(definition["listing-identifier"], MetaBool):
1120
            defined[category]["listing-identifier"] = definition[
1121
                "listing-identifier"
1122
            ].boolean
1123
        elif (
1124
            isinstance(definition["listing-identifier"], MetaInlines)
1125
            and len(definition["listing-identifier"].content) == 1
1126
            and isinstance(definition["listing-identifier"].content[0], Str)
1127
        ):
1128
            defined[category]["listing-identifier"] = (
1129
                definition["listing-identifier"].content[0].text
1130
            )
1131
        else:
1132
            debug(
1133
                "[WARNING] pandoc-numbering: "
1134
                "listing-identifier is not correct for category " + category
1135
            )
1136
1137
1138 View Code Duplication
def meta_format_text(category, definition, defined):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
1139
    """
1140
    Compute format text for a category.
1141
1142
    Arguments
1143
    ---------
1144
    category
1145
        The category
1146
    definition
1147
        The definition
1148
    defined
1149
        The defined parameter
1150
    """
1151
    if "format-text-classic" in definition:
1152
        if isinstance(definition["format-text-classic"], MetaInlines):
1153
            defined[category]["format-text-classic"] = definition[
1154
                "format-text-classic"
1155
            ].content
1156
            # Detach from original parent
1157
            defined[category]["format-text-classic"].parent = None
1158
        else:
1159
            debug(
1160
                "[WARNING] pandoc-numbering: "
1161
                "format-text-classic is not correct for category " + category
1162
            )
1163
1164
    if "format-text-title" in definition:
1165
        if isinstance(definition["format-text-title"], MetaInlines):
1166
            defined[category]["format-text-title"] = definition[
1167
                "format-text-title"
1168
            ].content
1169
            # Detach from original parent
1170
            defined[category]["format-text-title"].parent = None
1171
        else:
1172
            debug(
1173
                "[WARNING] pandoc-numbering: "
1174
                "format-text-title is not correct for category " + category
1175
            )
1176
1177
1178 View Code Duplication
def meta_format_link(category, definition, defined):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
1179
    """
1180
    Compute format link for a category.
1181
1182
    Arguments
1183
    ---------
1184
    category
1185
        The category
1186
    definition
1187
        The definition
1188
    defined
1189
        The defined parameter
1190
    """
1191
    if "format-link-classic" in definition:
1192
        if isinstance(definition["format-link-classic"], MetaInlines):
1193
            defined[category]["format-link-classic"] = definition[
1194
                "format-link-classic"
1195
            ].content
1196
            # Detach from original parent
1197
            defined[category]["format-link-classic"].parent = None
1198
        else:
1199
            debug(
1200
                "[WARNING] pandoc-numbering: "
1201
                "format-link-classic is not correct for category " + category
1202
            )
1203
1204
    if "format-link-title" in definition:
1205
        if isinstance(definition["format-link-title"], MetaInlines):
1206
            defined[category]["format-link-title"] = definition[
1207
                "format-link-title"
1208
            ].content
1209
            # Detach from original parent
1210
            defined[category]["format-link-title"].parent = None
1211
        else:
1212
            debug(
1213
                "[WARNING] pandoc-numbering: "
1214
                "format-link-title is not correct for category " + category
1215
            )
1216
1217
1218
def meta_format_caption(category, definition, defined):
1219
    """
1220
    Compute format caption for a category.
1221
1222
    Arguments
1223
    ---------
1224
    category
1225
        The category
1226
    definition
1227
        The definition
1228
    defined
1229
        The defined parameter
1230
    """
1231 View Code Duplication
    if "format-caption-classic" in definition:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
1232
        if isinstance(definition["format-caption-classic"], MetaInlines):
1233
            defined[category]["format-caption-classic"] = stringify(
1234
                definition["format-caption-classic"]
1235
            )
1236
        else:
1237
            debug(
1238
                "[WARNING] pandoc-numbering: "
1239
                "format-caption-classic is not correct for category " + category
1240
            )
1241
1242 View Code Duplication
    if "format-caption-title" in definition:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
1243
        if isinstance(definition["format-caption-title"], MetaInlines):
1244
            defined[category]["format-caption-title"] = stringify(
1245
                definition["format-caption-title"]
1246
            )
1247
        else:
1248
            debug(
1249
                "[WARNING] pandoc-numbering: "
1250
                "format-caption-title is not correct for category " + category
1251
            )
1252
1253
1254 View Code Duplication
def meta_format_entry(category, definition, defined):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
1255
    """
1256
    Compute format entry for a category.
1257
1258
    Arguments
1259
    ---------
1260
    category
1261
        The category
1262
    definition
1263
        The definition
1264
    defined
1265
        The defined parameter
1266
    """
1267
    if "format-entry-classic" in definition:
1268
        if isinstance(definition["format-entry-classic"], MetaInlines):
1269
            defined[category]["format-entry-classic"] = definition[
1270
                "format-entry-classic"
1271
            ].content
1272
            # Detach from original parent
1273
            defined[category]["format-entry-classic"].parent = None
1274
        else:
1275
            debug(
1276
                "[WARNING] pandoc-numbering: "
1277
                "format-entry-classic is not correct for category " + category
1278
            )
1279
1280
    if "format-entry-title" in definition:
1281
        if isinstance(definition["format-entry-title"], MetaInlines):
1282
            defined[category]["format-entry-title"] = definition[
1283
                "format-entry-title"
1284
            ].content
1285
            # Detach from original parent
1286
            defined[category]["format-entry-title"].parent = None
1287
        else:
1288
            debug(
1289
                "[WARNING] pandoc-numbering: "
1290
                "format-entry-title is not correct for category " + category
1291
            )
1292
1293
1294 View Code Duplication
def meta_entry_tab(category, definition, defined):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
1295
    """
1296
    Compute entry tab for a category.
1297
1298
    Arguments
1299
    ---------
1300
    category
1301
        The category
1302
    definition
1303
        The definition
1304
    defined
1305
        The defined parameter
1306
    """
1307
    if "entry-tab" in definition:
1308
        if isinstance(definition["entry-tab"], MetaString):
1309
            value = definition["entry-tab"].text
1310
        elif (
1311
            isinstance(definition["entry-tab"], MetaInlines)
1312
            and len(definition["entry-tab"].content) == 1
1313
        ):
1314
            value = definition["entry-tab"].content[0].text
1315
        else:
1316
            debug(
1317
                "[WARNING] pandoc-numbering: entry-tab is not correct for category "
1318
                + category
1319
            )
1320
            return
1321
        # Get the tab
1322
        try:
1323
            tab = float(value)
1324
            if tab > 0:
1325
                defined[category]["entry-tab"] = tab
1326
            else:
1327
                debug(
1328
                    "[WARNING] pandoc-numbering: "
1329
                    "entry-tab must be positive for category " + category
1330
                )
1331
        except ValueError:
1332
            debug(
1333
                "[WARNING] pandoc-numbering: entry-tab is not correct for category "
1334
                + category
1335
            )
1336
1337
1338 View Code Duplication
def meta_entry_space(category, definition, defined):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
1339
    """
1340
    Compute entry space for a category.
1341
1342
    Arguments
1343
    ---------
1344
    category
1345
        The category
1346
    definition
1347
        The definition
1348
    defined
1349
        The defined parameter
1350
    """
1351
    if "entry-space" in definition:
1352
        if isinstance(definition["entry-space"], MetaString):
1353
            value = definition["entry-space"].text
1354
        elif (
1355
            isinstance(definition["entry-space"], MetaInlines)
1356
            and len(definition["entry-space"].content) == 1
1357
        ):
1358
            value = definition["entry-space"].content[0].text
1359
        else:
1360
            debug(
1361
                "[WARNING] pandoc-numbering: entry-space is not correct for category "
1362
                + category
1363
            )
1364
            return
1365
        # Get the space
1366
        try:
1367
            space = float(value)
1368
            if space > 0:
1369
                defined[category]["entry-space"] = space
1370
            else:
1371
                debug(
1372
                    "[WARNING] pandoc-numbering: "
1373
                    "entry-space must be positive for category " + category
1374
                )
1375
        except ValueError:
1376
            debug(
1377
                "[WARNING] pandoc-numbering: entry-space is not correct for category "
1378
                + category
1379
            )
1380
1381
1382
def meta_levels(category, definition, defined):
1383
    """
1384
    Compute level for a category.
1385
1386
    Arguments
1387
    ---------
1388
    category
1389
        The category
1390
    definition
1391
        The definition
1392
    defined
1393
        The defined parameter
1394
    """
1395
    if (
1396
        "sectioning-levels" in definition
1397
        and isinstance(definition["sectioning-levels"], MetaInlines)
1398
        and len(definition["sectioning-levels"].content) == 1
1399
    ):
1400
        match = re.match(
1401
            Numbered.header_regex, definition["sectioning-levels"].content[0].text
1402
        )
1403
        if match:
1404
            # Compute the first and last levels section
1405
            defined[category]["first-section-level"] = len(match.group("hidden")) // 2
1406
            defined[category]["last-section-level"] = len(match.group("header")) // 2
1407
    if "first-section-level" in definition:
1408
        if isinstance(definition["first-section-level"], MetaString):
1409
            value = definition["first-section-level"].text
1410
        elif (
1411
            isinstance(definition["first-section-level"], MetaInlines)
1412
            and len(definition["first-section-level"].content) == 1
1413
        ):
1414
            value = definition["first-section-level"].content[0].text
1415
        else:
1416
            debug(
1417
                "[WARNING] pandoc-numbering: "
1418
                "first-section-level is not correct for category " + category
1419
            )
1420
            return
1421
1422
        # Get the level
1423
        try:
1424
            level = int(value) - 1
1425
        except ValueError:
1426
            debug(
1427
                "[WARNING] pandoc-numbering: "
1428
                "first-section-level is not correct for category " + category
1429
            )
1430
1431
        if 0 <= level <= 6:
1432
            defined[category]["first-section-level"] = level
1433
        else:
1434
            # pylint: disable=line-too-long
1435
            debug(
1436
                "[WARNING] pandoc-numbering: "
1437
                "first-section-level must be positive or zero for category " + category
1438
            )
1439
1440
    if "last-section-level" in definition:
1441
        if isinstance(definition["last-section-level"], MetaString):
1442
            value = definition["last-section-level"].text
1443
        elif (
1444
            isinstance(definition["last-section-level"], MetaInlines)
1445
            and len(definition["last-section-level"].content) == 1
1446
        ):
1447
            value = definition["last-section-level"].content[0].text
1448
        else:
1449
            debug(
1450
                "[WARNING] pandoc-numbering: "
1451
                "last-section-level is not correct for category " + category
1452
            )
1453
            return
1454
1455
        # Get the level
1456
        try:
1457
            level = int(value)
1458
        except ValueError:
1459
            debug(
1460
                "[WARNING] pandoc-numbering: "
1461
                "last-section-level is not correct for category " + category
1462
            )
1463
1464
        if 0 <= level <= 6:
1465
            defined[category]["last-section-level"] = level
1466
        else:
1467
            # pylint: disable=line-too-long
1468
            debug(
1469
                "[WARNING] pandoc-numbering: "
1470
                "last-section-level must be positive or zero for category " + category
1471
            )
1472
1473
1474
def meta_classes(category, definition, defined):
1475
    """
1476
    Compute classes for a category.
1477
1478
    Arguments
1479
    ---------
1480
    category
1481
        The category
1482
    definition
1483
        The definition
1484
    defined
1485
        The defined parameter
1486
    """
1487
    if "classes" in definition and isinstance(definition["classes"], MetaList):
1488
        defined[category]["classes"] = [
1489
            stringify(elt) for elt in definition["classes"].content
1490
        ]
1491
1492
1493
def finalize(doc):
1494
    """
1495
    Finalize document.
1496
1497
    Arguments
1498
    ---------
1499
    doc
1500
        The pandoc document
1501
    """
1502
    # Loop on all listings definition
1503
1504
    if doc.format in {"tex", "latex"}:
1505
        # Add header-includes if necessary
1506
        if "header-includes" not in doc.metadata:
1507
            doc.metadata["header-includes"] = MetaList()
1508
        # Convert header-includes to MetaList if necessary
1509
        elif not isinstance(doc.metadata["header-includes"], MetaList):
1510
            doc.metadata["header-includes"] = MetaList(doc.metadata["header-includes"])
1511
1512
        doc.metadata["header-includes"].append(
1513
            MetaInlines(
1514
                RawInline(
1515
                    dedent(
1516
                        r"""
1517
                        \makeatletter
1518
                        \@ifpackageloaded{subfig}{
1519
                            \usepackage[subfigure]{tocloft}
1520
                        }{
1521
                            \usepackage{tocloft}
1522
                        }
1523
                        \makeatother
1524
                        """
1525
                    ),
1526
                    "tex",
1527
                )
1528
            )
1529
        )
1530
        doc.metadata["header-includes"].append(
1531
            MetaInlines(RawInline(r"\usepackage{etoolbox}", "tex"))
1532
        )
1533
1534
    i = 0
1535
    listof = []
1536
    for category, definition in doc.defined.items():
1537
        if definition["listing-title"] is not None:
1538
            # pylint: disable=consider-using-f-string
1539
            if doc.format in {"tex", "latex"}:
1540
                latex_category = re.sub("[^a-z]+", "", category)
1541
                latex = (
1542
                    r"\newlistof{%s}{%s}{%s}"
1543
                    r"\renewcommand{\cft%stitlefont}{\cfttoctitlefont}"
1544
                    r"\setlength{\cft%snumwidth}{\cftfignumwidth}"
1545
                    r"\setlength{\cft%sindent}{\cftfigindent}"
1546
                    % (
1547
                        latex_category,
1548
                        latex_category,
1549
                        convert_text(
1550
                            Plain(*definition["listing-title"]),
1551
                            input_format="panflute",
1552
                            output_format="latex",
1553
                        ),
1554
                        latex_category,
1555
                        latex_category,
1556
                        latex_category,
1557
                    )
1558
                )
1559
                doc.metadata["header-includes"].append(
1560
                    MetaInlines(RawInline(latex, "tex"))
1561
                )
1562
                listof.append(f"\\listof{latex_category}")
1563
            else:
1564
                classes = ["pandoc-numbering-listing"] + definition["classes"]
1565
1566
                if definition["listing-unnumbered"]:
1567
                    classes.append("unnumbered")
1568
1569
                if definition["listing-unlisted"]:
1570
                    classes.append("unlisted")
1571
1572
                if definition["listing-identifier"] is False:
1573
                    header = Header(
1574
                        *definition["listing-title"], level=1, classes=classes
1575
                    )
1576
                elif definition["listing-identifier"] is True:
1577
                    header = Header(
1578
                        *definition["listing-title"], level=1, classes=classes
1579
                    )
1580
                    header = convert_text(
1581
                        convert_text(
1582
                            header, input_format="panflute", output_format="markdown"
1583
                        ),
1584
                        output_format="panflute",
1585
                    )[0]
1586
                else:
1587
                    header = Header(
1588
                        *definition["listing-title"],
1589
                        level=1,
1590
                        classes=classes,
1591
                        identifier=definition["listing-identifier"],
1592
                    )
1593
1594
                doc.content.insert(i, header)
1595
                i = i + 1
1596
1597
                table = table_other(doc, category, definition)
1598
1599
                if table:
1600
                    doc.content.insert(i, table)
1601
                    i = i + 1
1602
1603
    if doc.format in {"tex", "latex"}:
1604
        header = (
1605
            r"\ifdef{\mainmatter}"
1606
            r"{\let\oldmainmatter\mainmatter"
1607
            r"\renewcommand{\mainmatter}[0]{%s\oldmainmatter}}"
1608
            r"{}"
1609
        )
1610
        doc.metadata["header-includes"].append(
1611
            MetaInlines(RawInline(header % "\n".join(listof), "tex"))
1612
        )
1613
1614
        latex = r"\ifdef{\mainmatter}{}{%s}"
1615
        doc.content.insert(0, RawBlock(latex % "\n".join(listof), "tex"))
1616
1617
1618
def table_other(doc, category, _):
1619
    """
1620
    Compute other code for table.
1621
1622
    Arguments
1623
    ---------
1624
    doc
1625
        pandoc document
1626
    category
1627
        category numbered
1628
1629
    Returns
1630
    -------
1631
        A BulletList or None
1632
    """
1633
    if category in doc.collections:
1634
        # Return a bullet list
1635
        return BulletList(
1636
            *(
1637
                ListItem(Plain(Link(doc.information[tag].entry, url="#" + tag)))
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable tag does not seem to be defined.
Loading history...
1638
                for tag in doc.collections[category]
1639
            )
1640
        )
1641
    return None
1642
1643
1644
def link_color(doc):
1645
    """
1646
    Compute LaTeX code for toc.
1647
1648
    Arguments
1649
    ---------
1650
    doc
1651
        pandoc document
1652
1653
    Returns
1654
    -------
1655
        LaTeX code for links.
1656
    """
1657
    # Get the link color
1658
    metadata = doc.get_metadata()
1659
    if "toccolor" in metadata:
1660
        return "\\hypersetup{linkcolor=" + str(metadata["toccolor"]) + "}"
1661
    return "\\hypersetup{linkcolor=black}"
1662
1663
1664
def main(doc=None) -> None:
1665
    """
1666
    Produce the final document.
1667
1668
    Parameters
1669
    ----------
1670
    doc
1671
        pandoc document
1672
    """
1673
    run_filters([numbering, referencing], prepare=prepare, doc=doc, finalize=finalize)
1674
1675
1676
if __name__ == "__main__":
1677
    main()
1678