xref: /linux/tools/lib/python/kdoc/kdoc_output.py (revision c991b7ef2fb658c186df56d16b3ebcd0afb555cc)
1#!/usr/bin/env python3
2# SPDX-License-Identifier: GPL-2.0
3# Copyright(c) 2025: Mauro Carvalho Chehab <mchehab@kernel.org>.
4#
5# pylint: disable=C0301,R0902,R0911,R0912,R0913,R0914,R0915,R0917
6
7"""
8Classes to implement output filters to print kernel-doc documentation.
9
10The implementation uses a virtual base class ``OutputFormat``. It
11contains dispatches to virtual methods, and some code to filter
12out output messages.
13
14The actual implementation is done on one separate class per each type
15of output, e.g. ``RestFormat`` and ``ManFormat`` classes.
16
17Currently, there are output classes for ReST and man/troff.
18"""
19
20import os
21import re
22from datetime import datetime
23
24from kdoc.kdoc_parser import KernelDoc, type_param
25from kdoc.kdoc_re import KernRe
26
27
28function_pointer = KernRe(r"([^\(]*\(\*)\s*\)\s*\(([^\)]*)\)", cache=False)
29
30# match expressions used to find embedded type information
31type_constant = KernRe(r"\b``([^\`]+)``\b", cache=False)
32type_constant2 = KernRe(r"\%([-_*\w]+)", cache=False)
33type_func = KernRe(r"(\w+)\(\)", cache=False)
34type_param_ref = KernRe(r"([\!~\*]?)\@(\w*((\.\w+)|(->\w+))*(\.\.\.)?)", cache=False)
35
36# Special RST handling for func ptr params
37type_fp_param = KernRe(r"\@(\w+)\(\)", cache=False)
38
39# Special RST handling for structs with func ptr params
40type_fp_param2 = KernRe(r"\@(\w+->\S+)\(\)", cache=False)
41
42type_env = KernRe(r"(\$\w+)", cache=False)
43type_enum = KernRe(r"\&(enum\s*([_\w]+))", cache=False)
44type_struct = KernRe(r"\&(struct\s*([_\w]+))", cache=False)
45type_typedef = KernRe(r"\&(typedef\s*([_\w]+))", cache=False)
46type_union = KernRe(r"\&(union\s*([_\w]+))", cache=False)
47type_member = KernRe(r"\&([_\w]+)(\.|->)([_\w]+)", cache=False)
48type_fallback = KernRe(r"\&([_\w]+)", cache=False)
49type_member_func = type_member + KernRe(r"\(\)", cache=False)
50
51
52class OutputFormat:
53    """
54    Base class for OutputFormat. If used as-is, it means that only
55    warnings will be displayed.
56    """
57
58    # output mode.
59    OUTPUT_ALL          = 0 #: Output all symbols and doc sections.
60    OUTPUT_INCLUDE      = 1 #: Output only specified symbols.
61    OUTPUT_EXPORTED     = 2 #: Output exported symbols.
62    OUTPUT_INTERNAL     = 3 #: Output non-exported symbols.
63
64    #: Highlights to be used in ReST format.
65    highlights = []
66
67    #: Blank line character.
68    blankline = ""
69
70    def __init__(self):
71        """Declare internal vars and set mode to ``OUTPUT_ALL``."""
72
73        self.out_mode = self.OUTPUT_ALL
74        self.enable_lineno = None
75        self.nosymbol = {}
76        self.symbol = None
77        self.function_table = None
78        self.config = None
79        self.no_doc_sections = False
80
81        self.data = ""
82
83    def set_config(self, config):
84        """
85        Setup global config variables used by both parser and output.
86        """
87
88        self.config = config
89
90    def set_filter(self, export, internal, symbol, nosymbol, function_table,
91                   enable_lineno, no_doc_sections):
92        """
93        Initialize filter variables according to the requested mode.
94
95        Only one choice is valid between export, internal and symbol.
96
97        The nosymbol filter can be used on all modes.
98        """
99
100        self.enable_lineno = enable_lineno
101        self.no_doc_sections = no_doc_sections
102        self.function_table = function_table
103
104        if symbol:
105            self.out_mode = self.OUTPUT_INCLUDE
106        elif export:
107            self.out_mode = self.OUTPUT_EXPORTED
108        elif internal:
109            self.out_mode = self.OUTPUT_INTERNAL
110        else:
111            self.out_mode = self.OUTPUT_ALL
112
113        if nosymbol:
114            self.nosymbol = set(nosymbol)
115
116
117    def highlight_block(self, block):
118        """
119        Apply the RST highlights to a sub-block of text.
120        """
121
122        for r, sub in self.highlights:
123            block = r.sub(sub, block)
124
125        return block
126
127    def out_warnings(self, args):
128        """
129        Output warnings for identifiers that will be displayed.
130        """
131
132        for log_msg in args.warnings:
133            self.config.warning(log_msg)
134
135    def check_doc(self, name, args):
136        """Check if DOC should be output."""
137
138        if self.no_doc_sections:
139            return False
140
141        if name in self.nosymbol:
142            return False
143
144        if self.out_mode == self.OUTPUT_ALL:
145            self.out_warnings(args)
146            return True
147
148        if self.out_mode == self.OUTPUT_INCLUDE:
149            if name in self.function_table:
150                self.out_warnings(args)
151                return True
152
153        return False
154
155    def check_declaration(self, dtype, name, args):
156        """
157        Checks if a declaration should be output or not based on the
158        filtering criteria.
159        """
160
161        if name in self.nosymbol:
162            return False
163
164        if self.out_mode == self.OUTPUT_ALL:
165            self.out_warnings(args)
166            return True
167
168        if self.out_mode in [self.OUTPUT_INCLUDE, self.OUTPUT_EXPORTED]:
169            if name in self.function_table:
170                return True
171
172        if self.out_mode == self.OUTPUT_INTERNAL:
173            if dtype != "function":
174                self.out_warnings(args)
175                return True
176
177            if name not in self.function_table:
178                self.out_warnings(args)
179                return True
180
181        return False
182
183    def msg(self, fname, name, args):
184        """
185        Handles a single entry from kernel-doc parser.
186        """
187
188        self.data = ""
189
190        dtype = args.type
191
192        if dtype == "doc":
193            self.out_doc(fname, name, args)
194            return self.data
195
196        if not self.check_declaration(dtype, name, args):
197            return self.data
198
199        if dtype == "function":
200            self.out_function(fname, name, args)
201            return self.data
202
203        if dtype == "enum":
204            self.out_enum(fname, name, args)
205            return self.data
206
207        if dtype == "var":
208            self.out_var(fname, name, args)
209            return self.data
210
211        if dtype == "typedef":
212            self.out_typedef(fname, name, args)
213            return self.data
214
215        if dtype in ["struct", "union"]:
216            self.out_struct(fname, name, args)
217            return self.data
218
219        # Warn if some type requires an output logic
220        self.config.log.warning("doesn't know how to output '%s' block",
221                                dtype)
222
223        return None
224
225    # Virtual methods to be overridden by inherited classes
226    # At the base class, those do nothing.
227    def set_symbols(self, symbols):
228        """Get a list of all symbols from kernel_doc."""
229
230    def out_doc(self, fname, name, args):
231        """Outputs a DOC block."""
232
233    def out_function(self, fname, name, args):
234        """Outputs a function."""
235
236    def out_enum(self, fname, name, args):
237        """Outputs an enum."""
238
239    def out_var(self, fname, name, args):
240        """Outputs a variable."""
241
242    def out_typedef(self, fname, name, args):
243        """Outputs a typedef."""
244
245    def out_struct(self, fname, name, args):
246        """Outputs a struct."""
247
248
249class RestFormat(OutputFormat):
250    """Consts and functions used by ReST output."""
251
252    #: Highlights to be used in ReST format
253    highlights = [
254        (type_constant, r"``\1``"),
255        (type_constant2, r"``\1``"),
256
257        # Note: need to escape () to avoid func matching later
258        (type_member_func, r":c:type:`\1\2\3\\(\\) <\1>`"),
259        (type_member, r":c:type:`\1\2\3 <\1>`"),
260        (type_fp_param, r"**\1\\(\\)**"),
261        (type_fp_param2, r"**\1\\(\\)**"),
262        (type_func, r"\1()"),
263        (type_enum, r":c:type:`\1 <\2>`"),
264        (type_struct, r":c:type:`\1 <\2>`"),
265        (type_typedef, r":c:type:`\1 <\2>`"),
266        (type_union, r":c:type:`\1 <\2>`"),
267
268        # in rst this can refer to any type
269        (type_fallback, r":c:type:`\1`"),
270        (type_param_ref, r"**\1\2**")
271    ]
272
273    blankline = "\n"
274
275    #: Sphinx literal block regex.
276    sphinx_literal = KernRe(r'^[^.].*::$', cache=False)
277
278    #: Sphinx code block regex.
279    sphinx_cblock = KernRe(r'^\.\.\ +code-block::', cache=False)
280
281    def __init__(self):
282        """
283        Creates class variables.
284
285        Not really mandatory, but it is a good coding style and makes
286        pylint happy.
287        """
288
289        super().__init__()
290        self.lineprefix = ""
291
292    def print_lineno(self, ln):
293        """Outputs a line number."""
294
295        if self.enable_lineno and ln is not None:
296            ln += 1
297            self.data += f".. LINENO {ln}\n"
298
299    def output_highlight(self, args):
300        """
301        Outputs a C symbol that may require being converted to ReST using
302        the self.highlights variable.
303        """
304
305        input_text = args
306        output = ""
307        in_literal = False
308        litprefix = ""
309        block = ""
310
311        for line in input_text.strip("\n").split("\n"):
312
313            # If we're in a literal block, see if we should drop out of it.
314            # Otherwise, pass the line straight through unmunged.
315            if in_literal:
316                if line.strip():  # If the line is not blank
317                    # If this is the first non-blank line in a literal block,
318                    # figure out the proper indent.
319                    if not litprefix:
320                        r = KernRe(r'^(\s*)')
321                        if r.match(line):
322                            litprefix = '^' + r.group(1)
323                        else:
324                            litprefix = ""
325
326                        output += line + "\n"
327                    elif not KernRe(litprefix).match(line):
328                        in_literal = False
329                    else:
330                        output += line + "\n"
331                else:
332                    output += line + "\n"
333
334            # Not in a literal block (or just dropped out)
335            if not in_literal:
336                block += line + "\n"
337                if self.sphinx_literal.match(line) or self.sphinx_cblock.match(line):
338                    in_literal = True
339                    litprefix = ""
340                    output += self.highlight_block(block)
341                    block = ""
342
343        # Handle any remaining block
344        if block:
345            output += self.highlight_block(block)
346
347        # Print the output with the line prefix
348        for line in output.strip("\n").split("\n"):
349            self.data += self.lineprefix + line + "\n"
350
351    def out_section(self, args, out_docblock=False):
352        """
353        Outputs a block section.
354
355        This could use some work; it's used to output the DOC: sections, and
356        starts by putting out the name of the doc section itself, but that
357        tends to duplicate a header already in the template file.
358        """
359        for section, text in args.sections.items():
360            # Skip sections that are in the nosymbol_table
361            if section in self.nosymbol:
362                continue
363
364            if out_docblock:
365                if not self.out_mode == self.OUTPUT_INCLUDE:
366                    self.data += f".. _{section}:\n\n"
367                    self.data += f'{self.lineprefix}**{section}**\n\n'
368            else:
369                self.data += f'{self.lineprefix}**{section}**\n\n'
370
371            self.print_lineno(args.section_start_lines.get(section, 0))
372            self.output_highlight(text)
373            self.data += "\n"
374        self.data += "\n"
375
376    def out_doc(self, fname, name, args):
377        if not self.check_doc(name, args):
378            return
379        self.out_section(args, out_docblock=True)
380
381    def out_function(self, fname, name, args):
382
383        oldprefix = self.lineprefix
384        signature = ""
385
386        func_macro = args.get('func_macro', False)
387        if func_macro:
388            signature = name
389        else:
390            if args.get('functiontype'):
391                signature = args['functiontype'] + " "
392            signature += name + " ("
393
394        ln = args.declaration_start_line
395        count = 0
396        for parameter in args.parameterlist:
397            if count != 0:
398                signature += ", "
399            count += 1
400            dtype = args.parametertypes.get(parameter, "")
401
402            if function_pointer.search(dtype):
403                signature += function_pointer.group(1) + parameter + function_pointer.group(3)
404            else:
405                signature += dtype
406
407        if not func_macro:
408            signature += ")"
409
410        self.print_lineno(ln)
411        if args.get('typedef') or not args.get('functiontype'):
412            self.data += f".. c:macro:: {name}\n\n"
413
414            if args.get('typedef'):
415                self.data += "   **Typedef**: "
416                self.lineprefix = ""
417                self.output_highlight(args.get('purpose', ""))
418                self.data += "\n\n**Syntax**\n\n"
419                self.data += f"  ``{signature}``\n\n"
420            else:
421                self.data += f"``{signature}``\n\n"
422        else:
423            self.data += f".. c:function:: {signature}\n\n"
424
425        if not args.get('typedef'):
426            self.print_lineno(ln)
427            self.lineprefix = "   "
428            self.output_highlight(args.get('purpose', ""))
429            self.data += "\n"
430
431        # Put descriptive text into a container (HTML <div>) to help set
432        # function prototypes apart
433        self.lineprefix = "  "
434
435        if args.parameterlist:
436            self.data += ".. container:: kernelindent\n\n"
437            self.data += f"{self.lineprefix}**Parameters**\n\n"
438
439        for parameter in args.parameterlist:
440            parameter_name = KernRe(r'\[.*').sub('', parameter)
441            dtype = args.parametertypes.get(parameter, "")
442
443            if dtype:
444                self.data += f"{self.lineprefix}``{dtype}``\n"
445            else:
446                self.data += f"{self.lineprefix}``{parameter}``\n"
447
448            self.print_lineno(args.parameterdesc_start_lines.get(parameter_name, 0))
449
450            self.lineprefix = "    "
451            if parameter_name in args.parameterdescs and \
452               args.parameterdescs[parameter_name] != KernelDoc.undescribed:
453
454                self.output_highlight(args.parameterdescs[parameter_name])
455                self.data += "\n"
456            else:
457                self.data += f"{self.lineprefix}*undescribed*\n\n"
458            self.lineprefix = "  "
459
460        self.out_section(args)
461        self.lineprefix = oldprefix
462
463    def out_enum(self, fname, name, args):
464
465        oldprefix = self.lineprefix
466        ln = args.declaration_start_line
467
468        self.data += f"\n\n.. c:enum:: {name}\n\n"
469
470        self.print_lineno(ln)
471        self.lineprefix = "  "
472        self.output_highlight(args.get('purpose', ''))
473        self.data += "\n"
474
475        self.data += ".. container:: kernelindent\n\n"
476        outer = self.lineprefix + "  "
477        self.lineprefix = outer + "  "
478        self.data += f"{outer}**Constants**\n\n"
479
480        for parameter in args.parameterlist:
481            self.data += f"{outer}``{parameter}``\n"
482
483            if args.parameterdescs.get(parameter, '') != KernelDoc.undescribed:
484                self.output_highlight(args.parameterdescs[parameter])
485            else:
486                self.data += f"{self.lineprefix}*undescribed*\n\n"
487            self.data += "\n"
488
489        self.lineprefix = oldprefix
490        self.out_section(args)
491
492    def out_var(self, fname, name, args):
493        oldprefix = self.lineprefix
494        ln = args.declaration_start_line
495        full_proto = args.other_stuff["full_proto"]
496
497        self.lineprefix = "  "
498
499        self.data += f"\n\n.. c:macro:: {name}\n\n{self.lineprefix}``{full_proto}``\n\n"
500
501        self.print_lineno(ln)
502        self.output_highlight(args.get('purpose', ''))
503        self.data += "\n"
504
505        if args.other_stuff["default_val"]:
506            self.data += f'{self.lineprefix}**Initialization**\n\n'
507            self.output_highlight(f'default: ``{args.other_stuff["default_val"]}``')
508
509        self.out_section(args)
510
511    def out_typedef(self, fname, name, args):
512
513        oldprefix = self.lineprefix
514        ln = args.declaration_start_line
515
516        self.data += f"\n\n.. c:type:: {name}\n\n"
517
518        self.print_lineno(ln)
519        self.lineprefix = "   "
520
521        self.output_highlight(args.get('purpose', ''))
522
523        self.data += "\n"
524
525        self.lineprefix = oldprefix
526        self.out_section(args)
527
528    def out_struct(self, fname, name, args):
529
530        purpose = args.get('purpose', "")
531        declaration = args.get('definition', "")
532        dtype = args.type
533        ln = args.declaration_start_line
534
535        self.data += f"\n\n.. c:{dtype}:: {name}\n\n"
536
537        self.print_lineno(ln)
538
539        oldprefix = self.lineprefix
540        self.lineprefix += "  "
541
542        self.output_highlight(purpose)
543        self.data += "\n"
544
545        self.data += ".. container:: kernelindent\n\n"
546        self.data += f"{self.lineprefix}**Definition**::\n\n"
547
548        self.lineprefix = self.lineprefix + "  "
549
550        declaration = declaration.replace("\t", self.lineprefix)
551
552        self.data += f"{self.lineprefix}{dtype} {name}" + ' {' + "\n"
553        self.data += f"{declaration}{self.lineprefix}" + "};\n\n"
554
555        self.lineprefix = "  "
556        self.data += f"{self.lineprefix}**Members**\n\n"
557        for parameter in args.parameterlist:
558            if not parameter or parameter.startswith("#"):
559                continue
560
561            parameter_name = parameter.split("[", maxsplit=1)[0]
562
563            if args.parameterdescs.get(parameter_name) == KernelDoc.undescribed:
564                continue
565
566            self.print_lineno(args.parameterdesc_start_lines.get(parameter_name, 0))
567
568            self.data += f"{self.lineprefix}``{parameter}``\n"
569
570            self.lineprefix = "    "
571            self.output_highlight(args.parameterdescs[parameter_name])
572            self.lineprefix = "  "
573
574            self.data += "\n"
575
576        self.data += "\n"
577
578        self.lineprefix = oldprefix
579        self.out_section(args)
580
581
582class ManFormat(OutputFormat):
583    """
584    Consts and functions used by man pages output.
585
586    This class has one mandatory parameter and some optional ones, which
587    are needed to define the title header contents:
588
589    ``modulename``
590        Defines the module name to be used at the troff ``.TH`` output.
591
592        This argument is optional. If not specified, it will be filled
593        with the directory which contains the documented file.
594
595    ``section``
596        Usually a numeric value from 0 to 9, but man pages also accept
597        some strings like "p".
598
599        Defauls to ``9``
600
601    ``manual``
602        Defaults to ``Kernel API Manual``.
603
604    The above controls the output of teh corresponding fields on troff
605    title headers, which will be filled like this::
606
607        .TH "{name}" {section} "{date}" "{modulename}" "{manual}"
608
609    where ``name``` will match the API symbol name, and ``date`` will be
610    either the date where the Kernel was compiled or the current date
611    """
612
613    highlights = (
614        (type_constant, r"\1"),
615        (type_constant2, r"\1"),
616        (type_func, r"\\fB\1\\fP"),
617        (type_enum, r"\\fI\1\\fP"),
618        (type_struct, r"\\fI\1\\fP"),
619        (type_typedef, r"\\fI\1\\fP"),
620        (type_union, r"\\fI\1\\fP"),
621        (type_param, r"\\fI\1\\fP"),
622        (type_param_ref, r"\\fI\1\2\\fP"),
623        (type_member, r"\\fI\1\2\3\\fP"),
624        (type_fallback, r"\\fI\1\\fP")
625    )
626    blankline = ""
627
628    #: Allowed timestamp formats.
629    date_formats = [
630        "%a %b %d %H:%M:%S %Z %Y",
631        "%a %b %d %H:%M:%S %Y",
632        "%Y-%m-%d",
633        "%b %d %Y",
634        "%B %d %Y",
635        "%m %d %Y",
636    ]
637
638    def modulename(self, args):
639        if self._modulename:
640            return self._modulename
641
642        return os.path.dirname(args.fname)
643
644    def emit_th(self, name, args):
645        """Emit a title header line."""
646        title = name.strip()
647        module = self.modulename(args)
648
649        self.data += f'.TH "{title}" {self.section} "{self.date}" '
650        self.data += f'"{module}" "{self.manual}"\n'
651
652    def __init__(self, modulename=None, section="9", manual="Kernel API Manual"):
653        """
654        Creates class variables.
655
656        Not really mandatory, but it is a good coding style and makes
657        pylint happy.
658        """
659
660        super().__init__()
661
662        self._modulename = modulename
663        self.section = section
664        self.manual = manual
665
666        self.symbols = []
667
668        dt = None
669        tstamp = os.environ.get("KBUILD_BUILD_TIMESTAMP")
670        if tstamp:
671            for fmt in self.date_formats:
672                try:
673                    dt = datetime.strptime(tstamp, fmt)
674                    break
675                except ValueError:
676                    pass
677
678        if not dt:
679            dt = datetime.now()
680
681        self.date = dt.strftime("%B %Y")
682
683    def arg_name(self, args, name):
684        """
685        Return the name that will be used for the man page.
686
687        As we may have the same name on different namespaces,
688        prepend the data type for all types except functions and typedefs.
689
690        The doc section is special: it uses the modulename.
691        """
692
693        dtype = args.type
694
695        if dtype == "doc":
696            return name
697#            return os.path.basename(self.modulename(args))
698
699        if dtype in ["function", "typedef"]:
700            return name
701
702        return f"{dtype} {name}"
703
704    def set_symbols(self, symbols):
705        """
706        Get a list of all symbols from kernel_doc.
707
708        Man pages will uses it to add a SEE ALSO section with other
709        symbols at the same file.
710        """
711        self.symbols = symbols
712
713    def out_tail(self, fname, name, args):
714        """Adds a tail for all man pages."""
715
716        # SEE ALSO section
717        self.data += f'.SH "SEE ALSO"' + "\n.PP\n"
718        self.data += (f"Kernel file \\fB{args.fname}\\fR\n")
719        if len(self.symbols) >= 2:
720            cur_name = self.arg_name(args, name)
721
722            related = []
723            for arg in self.symbols:
724                out_name = self.arg_name(arg, arg.name)
725
726                if cur_name == out_name:
727                    continue
728
729                related.append(f"\\fB{out_name}\\fR(9)")
730
731            self.data += ",\n".join(related) + "\n"
732
733        # TODO: does it make sense to add other sections? Maybe
734        # REPORTING ISSUES? LICENSE?
735
736    def msg(self, fname, name, args):
737        """
738        Handles a single entry from kernel-doc parser.
739
740        Add a tail at the end of man pages output.
741        """
742        super().msg(fname, name, args)
743        self.out_tail(fname, name, args)
744
745        return self.data
746
747    def emit_table(self, colspec_row, rows):
748
749        if not rows:
750            return ""
751
752        out = ""
753        colspec = "\t".join(["l"] * len(rows[0]))
754
755        out += "\n.TS\n"
756        out += "box;\n"
757        out += f"{colspec}.\n"
758
759        if colspec_row:
760            out_row = []
761
762            for text in colspec_row:
763                out_row.append(f"\\fB{text}\\fP")
764
765            out += "\t".join(out_row) + "\n_\n"
766
767        for r in rows:
768            out += "\t".join(r) + "\n"
769
770        out += ".TE\n"
771
772        return out
773
774    def grid_table(self, lines, start):
775        """
776        Ancillary function to help handling a grid table inside the text.
777        """
778
779        i = start + 1
780        rows = []
781        colspec_row = None
782
783        while i < len(lines):
784            line = lines[i]
785
786            if KernRe(r"^\s*\|.*\|\s*$").match(line):
787                parts = []
788
789                for p in line.strip('|').split('|'):
790                    parts.append(p.strip())
791
792                rows.append(parts)
793
794            elif KernRe(r'^\+\=[\+\=]+\+\s*$').match(line):
795                if rows and rows[0]:
796                    if not colspec_row:
797                        colspec_row = [""] * len(rows[0])
798
799                    for j in range(0, len(rows[0])):
800                        content = []
801                        for row in rows:
802                            content.append(row[j])
803
804                        colspec_row[j] = " ".join(content)
805
806                    rows = []
807
808            elif KernRe(r"^\s*\+[-+]+\+.*$").match(line):
809                pass
810
811            else:
812                break
813
814            i += 1
815
816        return i, self.emit_table(colspec_row, rows)
817
818    def simple_table(self, lines, start):
819        """
820        Ancillary function to help handling a simple table inside the text.
821        """
822
823        i = start
824        rows = []
825        colspec_row = None
826
827        pos = []
828        for m in KernRe(r'\-+').finditer(lines[i]):
829            pos.append((m.start(), m.end() - 1))
830
831        i += 1
832        while i < len(lines):
833            line = lines[i]
834
835            if KernRe(r"^\s*[\-]+[ \t\-]+$").match(line):
836                i += 1
837                break
838
839            elif KernRe(r'^[\s=]+$').match(line):
840                if rows and rows[0]:
841                    if not colspec_row:
842                        colspec_row = [""] * len(rows[0])
843
844                    for j in range(0, len(rows[0])):
845                        content = []
846                        for row in rows:
847                            content.append(row[j])
848
849                        colspec_row[j] = " ".join(content)
850
851                    rows = []
852
853            else:
854                row = [""] * len(pos)
855
856                for j in range(0, len(pos)):
857                    start, end = pos[j]
858
859                    row[j] = line[start:end].strip()
860
861                rows.append(row)
862
863            i += 1
864
865        return i, self.emit_table(colspec_row, rows)
866
867    def code_block(self, lines, start):
868        """
869        Ensure that code blocks won't be messed up at the output.
870
871        By default, troff join lines at the same paragraph. Disable it,
872        on code blocks.
873        """
874
875        line = lines[start]
876
877        if "code-block" in line:
878            out = "\n.nf\n"
879        elif line.startswith("..") and line.endswith("::"):
880            #
881            # Handle note, warning, error, ... markups
882            #
883            line = line[2:-1].strip().upper()
884            out = f"\n.nf\n\\fB{line}\\fP\n"
885        elif line.endswith("::"):
886            out = line[:-1]
887            out += "\n.nf\n"
888        else:
889            # Just in case. Should never happen in practice
890            out = "\n.nf\n"
891
892        i = start + 1
893        ident = None
894
895        while i < len(lines):
896            line = lines[i]
897
898            m = KernRe(r"\S").match(line)
899            if not m:
900                out += line + "\n"
901                i += 1
902                continue
903
904            pos = m.start()
905            if not ident:
906                if pos > 0:
907                    ident = pos
908                else:
909                    out += "\n.fi\n"
910                    if i > start + 1:
911                        return i - 1, out
912                    else:
913                        # Just in case. Should never happen in practice
914                        return i, out
915
916            if pos >= ident:
917                out += line + "\n"
918                i += 1
919                continue
920
921            break
922
923        out += "\n.fi\n"
924        return i, out
925
926    def output_highlight(self, block):
927        """
928        Outputs a C symbol that may require being highlighted with
929        self.highlights variable using troff syntax.
930        """
931
932        contents = self.highlight_block(block)
933
934        if isinstance(contents, list):
935            contents = "\n".join(contents)
936
937        lines = contents.strip("\n").split("\n")
938        i = 0
939
940        while i < len(lines):
941            org_line = lines[i]
942
943            line = KernRe(r"^\s*").sub("", org_line)
944
945            if line:
946                if KernRe(r"^\+\-[-+]+\+.*$").match(line):
947                    i, text = self.grid_table(lines, i)
948                    self.data += text
949                    continue
950
951                if KernRe(r"^\-+[ \t]\-[ \t\-]+$").match(line):
952                    i, text = self.simple_table(lines, i)
953                    self.data += text
954                    continue
955
956                if line.endswith("::") or KernRe(r"\.\.\s+code-block.*::").match(line):
957                    i, text = self.code_block(lines, i)
958                    self.data += text
959                    continue
960
961                if line[0] == ".":
962                    self.data += "\\&" + line + "\n"
963                    i += 1
964                    continue
965
966                #
967                # Handle lists
968                #
969                line = KernRe(r'^[-*]\s+').sub(r'.IP \[bu]\n', line)
970                line = KernRe(r'^(\d+|a-z)[\.\)]\s+').sub(r'.IP \1\n', line)
971            else:
972                line = ".PP\n"
973
974            i += 1
975
976            self.data += line + "\n"
977
978    def out_doc(self, fname, name, args):
979        if not self.check_doc(name, args):
980            return
981
982        out_name = self.arg_name(args, name)
983
984        self.emit_th(out_name, args)
985
986        for section, text in args.sections.items():
987            self.data += f'.SH "{section}"' + "\n"
988            self.output_highlight(text)
989
990    def out_function(self, fname, name, args):
991
992        out_name = self.arg_name(args, name)
993
994        self.emit_th(out_name, args)
995
996        self.data += ".SH NAME\n"
997        self.data += f"{name} \\- {args['purpose']}\n"
998
999        self.data += ".SH SYNOPSIS\n"
1000        if args.get('functiontype', ''):
1001            self.data += f'.B "{args["functiontype"]}" {name}' + "\n"
1002        else:
1003            self.data += f'.B "{name}' + "\n"
1004
1005        count = 0
1006        parenth = "("
1007        post = ","
1008
1009        for parameter in args.parameterlist:
1010            if count == len(args.parameterlist) - 1:
1011                post = ");"
1012
1013            dtype = args.parametertypes.get(parameter, "")
1014            if function_pointer.match(dtype):
1015                # Pointer-to-function
1016                self.data += f'".BI "{parenth}{function_pointer.group(1)}" " ") ({function_pointer.group(2)}){post}"' + "\n"
1017            else:
1018                dtype = KernRe(r'([^\*])$').sub(r'\1 ', dtype)
1019
1020                self.data += f'.BI "{parenth}{dtype}"  "{post}"' + "\n"
1021            count += 1
1022            parenth = ""
1023
1024        if args.parameterlist:
1025            self.data += ".SH ARGUMENTS\n"
1026
1027        for parameter in args.parameterlist:
1028            parameter_name = re.sub(r'\[.*', '', parameter)
1029
1030            self.data += f'.IP "{parameter}" 12' + "\n"
1031            self.output_highlight(args.parameterdescs.get(parameter_name, ""))
1032
1033        for section, text in args.sections.items():
1034            self.data += f'.SH "{section.upper()}"' + "\n"
1035            self.output_highlight(text)
1036
1037    def out_enum(self, fname, name, args):
1038        out_name = self.arg_name(args, name)
1039
1040        self.emit_th(out_name, args)
1041
1042        self.data += ".SH NAME\n"
1043        self.data += f"enum {name} \\- {args['purpose']}\n"
1044
1045        self.data += ".SH SYNOPSIS\n"
1046        self.data += f"enum {name}" + " {\n"
1047
1048        count = 0
1049        for parameter in args.parameterlist:
1050            self.data += f'.br\n.BI "    {parameter}"' + "\n"
1051            if count == len(args.parameterlist) - 1:
1052                self.data += "\n};\n"
1053            else:
1054                self.data += ", \n.br\n"
1055
1056            count += 1
1057
1058        self.data += ".SH Constants\n"
1059
1060        for parameter in args.parameterlist:
1061            parameter_name = KernRe(r'\[.*').sub('', parameter)
1062            self.data += f'.IP "{parameter}" 12' + "\n"
1063            self.output_highlight(args.parameterdescs.get(parameter_name, ""))
1064
1065        for section, text in args.sections.items():
1066            self.data += f'.SH "{section}"' + "\n"
1067            self.output_highlight(text)
1068
1069    def out_var(self, fname, name, args):
1070        out_name = self.arg_name(args, name)
1071        full_proto = args.other_stuff["full_proto"]
1072
1073        self.emit_th(out_name, args)
1074
1075        self.data += ".SH NAME\n"
1076        self.data += f"{name} \\- {args['purpose']}\n"
1077
1078        self.data += ".SH SYNOPSIS\n"
1079        self.data += f"{full_proto}\n"
1080
1081        if args.other_stuff["default_val"]:
1082            self.data += f'.SH "Initialization"' + "\n"
1083            self.output_highlight(f'default: {args.other_stuff["default_val"]}')
1084
1085        for section, text in args.sections.items():
1086            self.data += f'.SH "{section}"' + "\n"
1087            self.output_highlight(text)
1088
1089    def out_typedef(self, fname, name, args):
1090        module = self.modulename(args)
1091        purpose = args.get('purpose')
1092        out_name = self.arg_name(args, name)
1093
1094        self.emit_th(out_name, args)
1095
1096        self.data += ".SH NAME\n"
1097        self.data += f"typedef {name} \\- {purpose}\n"
1098
1099        for section, text in args.sections.items():
1100            self.data += f'.SH "{section}"' + "\n"
1101            self.output_highlight(text)
1102
1103    def out_struct(self, fname, name, args):
1104        module = self.modulename(args)
1105        purpose = args.get('purpose')
1106        definition = args.get('definition')
1107        out_name = self.arg_name(args, name)
1108
1109        self.emit_th(out_name, args)
1110
1111        self.data += ".SH NAME\n"
1112        self.data += f"{args.type} {name} \\- {purpose}\n"
1113
1114        # Replace tabs with two spaces and handle newlines
1115        declaration = definition.replace("\t", "  ")
1116        declaration = KernRe(r"\n").sub('"\n.br\n.BI "', declaration)
1117
1118        self.data += ".SH SYNOPSIS\n"
1119        self.data += f"{args.type} {name} " + "{" + "\n.br\n"
1120        self.data += f'.BI "{declaration}\n' + "};\n.br\n\n"
1121
1122        self.data += ".SH Members\n"
1123        for parameter in args.parameterlist:
1124            if parameter.startswith("#"):
1125                continue
1126
1127            parameter_name = re.sub(r"\[.*", "", parameter)
1128
1129            if args.parameterdescs.get(parameter_name) == KernelDoc.undescribed:
1130                continue
1131
1132            self.data += f'.IP "{parameter}" 12' + "\n"
1133            self.output_highlight(args.parameterdescs.get(parameter_name))
1134
1135        for section, text in args.sections.items():
1136            self.data += f'.SH "{section}"' + "\n"
1137            self.output_highlight(text)
1138