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""" 8Implement output filters to print kernel-doc documentation. 9 10The implementation uses a virtual base class (OutputFormat) which 11contains a 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. Currently, there are output classes for ReST and man/troff. 16""" 17 18import os 19import re 20from datetime import datetime 21 22from kdoc_parser import KernelDoc, type_param 23from kdoc_re import KernRe 24 25 26function_pointer = KernRe(r"([^\(]*\(\*)\s*\)\s*\(([^\)]*)\)", cache=False) 27 28# match expressions used to find embedded type information 29type_constant = KernRe(r"\b``([^\`]+)``\b", cache=False) 30type_constant2 = KernRe(r"\%([-_*\w]+)", cache=False) 31type_func = KernRe(r"(\w+)\(\)", cache=False) 32type_param_ref = KernRe(r"([\!~\*]?)\@(\w*((\.\w+)|(->\w+))*(\.\.\.)?)", cache=False) 33 34# Special RST handling for func ptr params 35type_fp_param = KernRe(r"\@(\w+)\(\)", cache=False) 36 37# Special RST handling for structs with func ptr params 38type_fp_param2 = KernRe(r"\@(\w+->\S+)\(\)", cache=False) 39 40type_env = KernRe(r"(\$\w+)", cache=False) 41type_enum = KernRe(r"\&(enum\s*([_\w]+))", cache=False) 42type_struct = KernRe(r"\&(struct\s*([_\w]+))", cache=False) 43type_typedef = KernRe(r"\&(typedef\s*([_\w]+))", cache=False) 44type_union = KernRe(r"\&(union\s*([_\w]+))", cache=False) 45type_member = KernRe(r"\&([_\w]+)(\.|->)([_\w]+)", cache=False) 46type_fallback = KernRe(r"\&([_\w]+)", cache=False) 47type_member_func = type_member + KernRe(r"\(\)", cache=False) 48 49 50class OutputFormat: 51 """ 52 Base class for OutputFormat. If used as-is, it means that only 53 warnings will be displayed. 54 """ 55 56 # output mode. 57 OUTPUT_ALL = 0 # output all symbols and doc sections 58 OUTPUT_INCLUDE = 1 # output only specified symbols 59 OUTPUT_EXPORTED = 2 # output exported symbols 60 OUTPUT_INTERNAL = 3 # output non-exported symbols 61 62 # Virtual member to be overriden at the inherited classes 63 highlights = [] 64 65 def __init__(self): 66 """Declare internal vars and set mode to OUTPUT_ALL""" 67 68 self.out_mode = self.OUTPUT_ALL 69 self.enable_lineno = None 70 self.nosymbol = {} 71 self.symbol = None 72 self.function_table = None 73 self.config = None 74 self.no_doc_sections = False 75 76 self.data = "" 77 78 def set_config(self, config): 79 """ 80 Setup global config variables used by both parser and output. 81 """ 82 83 self.config = config 84 85 def set_filter(self, export, internal, symbol, nosymbol, function_table, 86 enable_lineno, no_doc_sections): 87 """ 88 Initialize filter variables according with the requested mode. 89 90 Only one choice is valid between export, internal and symbol. 91 92 The nosymbol filter can be used on all modes. 93 """ 94 95 self.enable_lineno = enable_lineno 96 self.no_doc_sections = no_doc_sections 97 self.function_table = function_table 98 99 if symbol: 100 self.out_mode = self.OUTPUT_INCLUDE 101 elif export: 102 self.out_mode = self.OUTPUT_EXPORTED 103 elif internal: 104 self.out_mode = self.OUTPUT_INTERNAL 105 else: 106 self.out_mode = self.OUTPUT_ALL 107 108 if nosymbol: 109 self.nosymbol = set(nosymbol) 110 111 112 def highlight_block(self, block): 113 """ 114 Apply the RST highlights to a sub-block of text. 115 """ 116 117 for r, sub in self.highlights: 118 block = r.sub(sub, block) 119 120 return block 121 122 def out_warnings(self, args): 123 """ 124 Output warnings for identifiers that will be displayed. 125 """ 126 127 warnings = args.get('warnings', []) 128 129 for log_msg in warnings: 130 self.config.warning(log_msg) 131 132 def check_doc(self, name, args): 133 """Check if DOC should be output""" 134 135 if self.no_doc_sections: 136 return False 137 138 if name in self.nosymbol: 139 return False 140 141 if self.out_mode == self.OUTPUT_ALL: 142 self.out_warnings(args) 143 return True 144 145 if self.out_mode == self.OUTPUT_INCLUDE: 146 if name in self.function_table: 147 self.out_warnings(args) 148 return True 149 150 return False 151 152 def check_declaration(self, dtype, name, args): 153 """ 154 Checks if a declaration should be output or not based on the 155 filtering criteria. 156 """ 157 158 if name in self.nosymbol: 159 return False 160 161 if self.out_mode == self.OUTPUT_ALL: 162 self.out_warnings(args) 163 return True 164 165 if self.out_mode in [self.OUTPUT_INCLUDE, self.OUTPUT_EXPORTED]: 166 if name in self.function_table: 167 return True 168 169 if self.out_mode == self.OUTPUT_INTERNAL: 170 if dtype != "function": 171 self.out_warnings(args) 172 return True 173 174 if name not in self.function_table: 175 self.out_warnings(args) 176 return True 177 178 return False 179 180 def msg(self, fname, name, args): 181 """ 182 Handles a single entry from kernel-doc parser 183 """ 184 185 self.data = "" 186 187 dtype = args.get('type', "") 188 189 if dtype == "doc": 190 self.out_doc(fname, name, args) 191 return self.data 192 193 if not self.check_declaration(dtype, name, args): 194 return self.data 195 196 if dtype == "function": 197 self.out_function(fname, name, args) 198 return self.data 199 200 if dtype == "enum": 201 self.out_enum(fname, name, args) 202 return self.data 203 204 if dtype == "typedef": 205 self.out_typedef(fname, name, args) 206 return self.data 207 208 if dtype in ["struct", "union"]: 209 self.out_struct(fname, name, args) 210 return self.data 211 212 # Warn if some type requires an output logic 213 self.config.log.warning("doesn't now how to output '%s' block", 214 dtype) 215 216 return None 217 218 # Virtual methods to be overridden by inherited classes 219 # At the base class, those do nothing. 220 def out_doc(self, fname, name, args): 221 """Outputs a DOC block""" 222 223 def out_function(self, fname, name, args): 224 """Outputs a function""" 225 226 def out_enum(self, fname, name, args): 227 """Outputs an enum""" 228 229 def out_typedef(self, fname, name, args): 230 """Outputs a typedef""" 231 232 def out_struct(self, fname, name, args): 233 """Outputs a struct""" 234 235 236class RestFormat(OutputFormat): 237 """Consts and functions used by ReST output""" 238 239 highlights = [ 240 (type_constant, r"``\1``"), 241 (type_constant2, r"``\1``"), 242 243 # Note: need to escape () to avoid func matching later 244 (type_member_func, r":c:type:`\1\2\3\\(\\) <\1>`"), 245 (type_member, r":c:type:`\1\2\3 <\1>`"), 246 (type_fp_param, r"**\1\\(\\)**"), 247 (type_fp_param2, r"**\1\\(\\)**"), 248 (type_func, r"\1()"), 249 (type_enum, r":c:type:`\1 <\2>`"), 250 (type_struct, r":c:type:`\1 <\2>`"), 251 (type_typedef, r":c:type:`\1 <\2>`"), 252 (type_union, r":c:type:`\1 <\2>`"), 253 254 # in rst this can refer to any type 255 (type_fallback, r":c:type:`\1`"), 256 (type_param_ref, r"**\1\2**") 257 ] 258 blankline = "\n" 259 260 sphinx_literal = KernRe(r'^[^.].*::$', cache=False) 261 sphinx_cblock = KernRe(r'^\.\.\ +code-block::', cache=False) 262 263 def __init__(self): 264 """ 265 Creates class variables. 266 267 Not really mandatory, but it is a good coding style and makes 268 pylint happy. 269 """ 270 271 super().__init__() 272 self.lineprefix = "" 273 274 def print_lineno(self, ln): 275 """Outputs a line number""" 276 277 if self.enable_lineno and ln is not None: 278 ln += 1 279 self.data += f".. LINENO {ln}\n" 280 281 def output_highlight(self, args): 282 """ 283 Outputs a C symbol that may require being converted to ReST using 284 the self.highlights variable 285 """ 286 287 input_text = args 288 output = "" 289 in_literal = False 290 litprefix = "" 291 block = "" 292 293 for line in input_text.strip("\n").split("\n"): 294 295 # If we're in a literal block, see if we should drop out of it. 296 # Otherwise, pass the line straight through unmunged. 297 if in_literal: 298 if line.strip(): # If the line is not blank 299 # If this is the first non-blank line in a literal block, 300 # figure out the proper indent. 301 if not litprefix: 302 r = KernRe(r'^(\s*)') 303 if r.match(line): 304 litprefix = '^' + r.group(1) 305 else: 306 litprefix = "" 307 308 output += line + "\n" 309 elif not KernRe(litprefix).match(line): 310 in_literal = False 311 else: 312 output += line + "\n" 313 else: 314 output += line + "\n" 315 316 # Not in a literal block (or just dropped out) 317 if not in_literal: 318 block += line + "\n" 319 if self.sphinx_literal.match(line) or self.sphinx_cblock.match(line): 320 in_literal = True 321 litprefix = "" 322 output += self.highlight_block(block) 323 block = "" 324 325 # Handle any remaining block 326 if block: 327 output += self.highlight_block(block) 328 329 # Print the output with the line prefix 330 for line in output.strip("\n").split("\n"): 331 self.data += self.lineprefix + line + "\n" 332 333 def out_section(self, args, out_docblock=False): 334 """ 335 Outputs a block section. 336 337 This could use some work; it's used to output the DOC: sections, and 338 starts by putting out the name of the doc section itself, but that 339 tends to duplicate a header already in the template file. 340 """ 341 for section, text in args.sections.items(): 342 # Skip sections that are in the nosymbol_table 343 if section in self.nosymbol: 344 continue 345 346 if out_docblock: 347 if not self.out_mode == self.OUTPUT_INCLUDE: 348 self.data += f".. _{section}:\n\n" 349 self.data += f'{self.lineprefix}**{section}**\n\n' 350 else: 351 self.data += f'{self.lineprefix}**{section}**\n\n' 352 353 self.print_lineno(args.section_start_lines.get(section, 0)) 354 self.output_highlight(text) 355 self.data += "\n" 356 self.data += "\n" 357 358 def out_doc(self, fname, name, args): 359 if not self.check_doc(name, args): 360 return 361 self.out_section(args, out_docblock=True) 362 363 def out_function(self, fname, name, args): 364 365 oldprefix = self.lineprefix 366 signature = "" 367 368 func_macro = args.get('func_macro', False) 369 if func_macro: 370 signature = args['function'] 371 else: 372 if args.get('functiontype'): 373 signature = args['functiontype'] + " " 374 signature += args['function'] + " (" 375 376 parameterlist = args.get('parameterlist', []) 377 parameterdescs = args.get('parameterdescs', {}) 378 parameterdesc_start_lines = args.get('parameterdesc_start_lines', {}) 379 380 ln = args.get('declaration_start_line', 0) 381 382 count = 0 383 for parameter in parameterlist: 384 if count != 0: 385 signature += ", " 386 count += 1 387 dtype = args['parametertypes'].get(parameter, "") 388 389 if function_pointer.search(dtype): 390 signature += function_pointer.group(1) + parameter + function_pointer.group(3) 391 else: 392 signature += dtype 393 394 if not func_macro: 395 signature += ")" 396 397 self.print_lineno(ln) 398 if args.get('typedef') or not args.get('functiontype'): 399 self.data += f".. c:macro:: {args['function']}\n\n" 400 401 if args.get('typedef'): 402 self.data += " **Typedef**: " 403 self.lineprefix = "" 404 self.output_highlight(args.get('purpose', "")) 405 self.data += "\n\n**Syntax**\n\n" 406 self.data += f" ``{signature}``\n\n" 407 else: 408 self.data += f"``{signature}``\n\n" 409 else: 410 self.data += f".. c:function:: {signature}\n\n" 411 412 if not args.get('typedef'): 413 self.print_lineno(ln) 414 self.lineprefix = " " 415 self.output_highlight(args.get('purpose', "")) 416 self.data += "\n" 417 418 # Put descriptive text into a container (HTML <div>) to help set 419 # function prototypes apart 420 self.lineprefix = " " 421 422 if parameterlist: 423 self.data += ".. container:: kernelindent\n\n" 424 self.data += f"{self.lineprefix}**Parameters**\n\n" 425 426 for parameter in parameterlist: 427 parameter_name = KernRe(r'\[.*').sub('', parameter) 428 dtype = args['parametertypes'].get(parameter, "") 429 430 if dtype: 431 self.data += f"{self.lineprefix}``{dtype}``\n" 432 else: 433 self.data += f"{self.lineprefix}``{parameter}``\n" 434 435 self.print_lineno(parameterdesc_start_lines.get(parameter_name, 0)) 436 437 self.lineprefix = " " 438 if parameter_name in parameterdescs and \ 439 parameterdescs[parameter_name] != KernelDoc.undescribed: 440 441 self.output_highlight(parameterdescs[parameter_name]) 442 self.data += "\n" 443 else: 444 self.data += f"{self.lineprefix}*undescribed*\n\n" 445 self.lineprefix = " " 446 447 self.out_section(args) 448 self.lineprefix = oldprefix 449 450 def out_enum(self, fname, name, args): 451 452 oldprefix = self.lineprefix 453 name = args.get('enum', '') 454 parameterlist = args.get('parameterlist', []) 455 parameterdescs = args.get('parameterdescs', {}) 456 ln = args.get('declaration_start_line', 0) 457 458 self.data += f"\n\n.. c:enum:: {name}\n\n" 459 460 self.print_lineno(ln) 461 self.lineprefix = " " 462 self.output_highlight(args.get('purpose', '')) 463 self.data += "\n" 464 465 self.data += ".. container:: kernelindent\n\n" 466 outer = self.lineprefix + " " 467 self.lineprefix = outer + " " 468 self.data += f"{outer}**Constants**\n\n" 469 470 for parameter in parameterlist: 471 self.data += f"{outer}``{parameter}``\n" 472 473 if parameterdescs.get(parameter, '') != KernelDoc.undescribed: 474 self.output_highlight(parameterdescs[parameter]) 475 else: 476 self.data += f"{self.lineprefix}*undescribed*\n\n" 477 self.data += "\n" 478 479 self.lineprefix = oldprefix 480 self.out_section(args) 481 482 def out_typedef(self, fname, name, args): 483 484 oldprefix = self.lineprefix 485 name = args.get('typedef', '') 486 ln = args.get('declaration_start_line', 0) 487 488 self.data += f"\n\n.. c:type:: {name}\n\n" 489 490 self.print_lineno(ln) 491 self.lineprefix = " " 492 493 self.output_highlight(args.get('purpose', '')) 494 495 self.data += "\n" 496 497 self.lineprefix = oldprefix 498 self.out_section(args) 499 500 def out_struct(self, fname, name, args): 501 502 name = args.get('struct', "") 503 purpose = args.get('purpose', "") 504 declaration = args.get('definition', "") 505 dtype = args.get('type', "struct") 506 ln = args.get('declaration_start_line', 0) 507 508 parameterlist = args.get('parameterlist', []) 509 parameterdescs = args.get('parameterdescs', {}) 510 parameterdesc_start_lines = args.get('parameterdesc_start_lines', {}) 511 512 self.data += f"\n\n.. c:{dtype}:: {name}\n\n" 513 514 self.print_lineno(ln) 515 516 oldprefix = self.lineprefix 517 self.lineprefix += " " 518 519 self.output_highlight(purpose) 520 self.data += "\n" 521 522 self.data += ".. container:: kernelindent\n\n" 523 self.data += f"{self.lineprefix}**Definition**::\n\n" 524 525 self.lineprefix = self.lineprefix + " " 526 527 declaration = declaration.replace("\t", self.lineprefix) 528 529 self.data += f"{self.lineprefix}{dtype} {name}" + ' {' + "\n" 530 self.data += f"{declaration}{self.lineprefix}" + "};\n\n" 531 532 self.lineprefix = " " 533 self.data += f"{self.lineprefix}**Members**\n\n" 534 for parameter in parameterlist: 535 if not parameter or parameter.startswith("#"): 536 continue 537 538 parameter_name = parameter.split("[", maxsplit=1)[0] 539 540 if parameterdescs.get(parameter_name) == KernelDoc.undescribed: 541 continue 542 543 self.print_lineno(parameterdesc_start_lines.get(parameter_name, 0)) 544 545 self.data += f"{self.lineprefix}``{parameter}``\n" 546 547 self.lineprefix = " " 548 self.output_highlight(parameterdescs[parameter_name]) 549 self.lineprefix = " " 550 551 self.data += "\n" 552 553 self.data += "\n" 554 555 self.lineprefix = oldprefix 556 self.out_section(args) 557 558 559class ManFormat(OutputFormat): 560 """Consts and functions used by man pages output""" 561 562 highlights = ( 563 (type_constant, r"\1"), 564 (type_constant2, r"\1"), 565 (type_func, r"\\fB\1\\fP"), 566 (type_enum, r"\\fI\1\\fP"), 567 (type_struct, r"\\fI\1\\fP"), 568 (type_typedef, r"\\fI\1\\fP"), 569 (type_union, r"\\fI\1\\fP"), 570 (type_param, r"\\fI\1\\fP"), 571 (type_param_ref, r"\\fI\1\2\\fP"), 572 (type_member, r"\\fI\1\2\3\\fP"), 573 (type_fallback, r"\\fI\1\\fP") 574 ) 575 blankline = "" 576 577 date_formats = [ 578 "%a %b %d %H:%M:%S %Z %Y", 579 "%a %b %d %H:%M:%S %Y", 580 "%Y-%m-%d", 581 "%b %d %Y", 582 "%B %d %Y", 583 "%m %d %Y", 584 ] 585 586 def __init__(self, modulename): 587 """ 588 Creates class variables. 589 590 Not really mandatory, but it is a good coding style and makes 591 pylint happy. 592 """ 593 594 super().__init__() 595 self.modulename = modulename 596 597 dt = None 598 tstamp = os.environ.get("KBUILD_BUILD_TIMESTAMP") 599 if tstamp: 600 for fmt in self.date_formats: 601 try: 602 dt = datetime.strptime(tstamp, fmt) 603 break 604 except ValueError: 605 pass 606 607 if not dt: 608 dt = datetime.now() 609 610 self.man_date = dt.strftime("%B %Y") 611 612 def output_highlight(self, block): 613 """ 614 Outputs a C symbol that may require being highlighted with 615 self.highlights variable using troff syntax 616 """ 617 618 contents = self.highlight_block(block) 619 620 if isinstance(contents, list): 621 contents = "\n".join(contents) 622 623 for line in contents.strip("\n").split("\n"): 624 line = KernRe(r"^\s*").sub("", line) 625 if not line: 626 continue 627 628 if line[0] == ".": 629 self.data += "\\&" + line + "\n" 630 else: 631 self.data += line + "\n" 632 633 def out_doc(self, fname, name, args): 634 if not self.check_doc(name, args): 635 return 636 637 self.data += f'.TH "{self.modulename}" 9 "{self.modulename}" "{self.man_date}" "API Manual" LINUX' + "\n" 638 639 for section, text in args.sections.items(): 640 self.data += f'.SH "{section}"' + "\n" 641 self.output_highlight(text) 642 643 def out_function(self, fname, name, args): 644 """output function in man""" 645 646 parameterlist = args.get('parameterlist', []) 647 parameterdescs = args.get('parameterdescs', {}) 648 649 self.data += f'.TH "{args["function"]}" 9 "{args["function"]}" "{self.man_date}" "Kernel Hacker\'s Manual" LINUX' + "\n" 650 651 self.data += ".SH NAME\n" 652 self.data += f"{args['function']} \\- {args['purpose']}\n" 653 654 self.data += ".SH SYNOPSIS\n" 655 if args.get('functiontype', ''): 656 self.data += f'.B "{args["functiontype"]}" {args["function"]}' + "\n" 657 else: 658 self.data += f'.B "{args["function"]}' + "\n" 659 660 count = 0 661 parenth = "(" 662 post = "," 663 664 for parameter in parameterlist: 665 if count == len(parameterlist) - 1: 666 post = ");" 667 668 dtype = args['parametertypes'].get(parameter, "") 669 if function_pointer.match(dtype): 670 # Pointer-to-function 671 self.data += f'".BI "{parenth}{function_pointer.group(1)}" " ") ({function_pointer.group(2)}){post}"' + "\n" 672 else: 673 dtype = KernRe(r'([^\*])$').sub(r'\1 ', dtype) 674 675 self.data += f'.BI "{parenth}{dtype}" "{post}"' + "\n" 676 count += 1 677 parenth = "" 678 679 if parameterlist: 680 self.data += ".SH ARGUMENTS\n" 681 682 for parameter in parameterlist: 683 parameter_name = re.sub(r'\[.*', '', parameter) 684 685 self.data += f'.IP "{parameter}" 12' + "\n" 686 self.output_highlight(parameterdescs.get(parameter_name, "")) 687 688 for section, text in args.sections.items(): 689 self.data += f'.SH "{section.upper()}"' + "\n" 690 self.output_highlight(text) 691 692 def out_enum(self, fname, name, args): 693 694 name = args.get('enum', '') 695 parameterlist = args.get('parameterlist', []) 696 697 self.data += f'.TH "{self.modulename}" 9 "enum {args["enum"]}" "{self.man_date}" "API Manual" LINUX' + "\n" 698 699 self.data += ".SH NAME\n" 700 self.data += f"enum {args['enum']} \\- {args['purpose']}\n" 701 702 self.data += ".SH SYNOPSIS\n" 703 self.data += f"enum {args['enum']}" + " {\n" 704 705 count = 0 706 for parameter in parameterlist: 707 self.data += f'.br\n.BI " {parameter}"' + "\n" 708 if count == len(parameterlist) - 1: 709 self.data += "\n};\n" 710 else: 711 self.data += ", \n.br\n" 712 713 count += 1 714 715 self.data += ".SH Constants\n" 716 717 for parameter in parameterlist: 718 parameter_name = KernRe(r'\[.*').sub('', parameter) 719 self.data += f'.IP "{parameter}" 12' + "\n" 720 self.output_highlight(args['parameterdescs'].get(parameter_name, "")) 721 722 for section, text in args.sections.items(): 723 self.data += f'.SH "{section}"' + "\n" 724 self.output_highlight(text) 725 726 def out_typedef(self, fname, name, args): 727 module = self.modulename 728 typedef = args.get('typedef') 729 purpose = args.get('purpose') 730 731 self.data += f'.TH "{module}" 9 "{typedef}" "{self.man_date}" "API Manual" LINUX' + "\n" 732 733 self.data += ".SH NAME\n" 734 self.data += f"typedef {typedef} \\- {purpose}\n" 735 736 for section, text in args.sections.items(): 737 self.data += f'.SH "{section}"' + "\n" 738 self.output_highlight(text) 739 740 def out_struct(self, fname, name, args): 741 module = self.modulename 742 struct_type = args.get('type') 743 struct_name = args.get('struct') 744 purpose = args.get('purpose') 745 definition = args.get('definition') 746 parameterlist = args.get('parameterlist', []) 747 parameterdescs = args.get('parameterdescs', {}) 748 749 self.data += f'.TH "{module}" 9 "{struct_type} {struct_name}" "{self.man_date}" "API Manual" LINUX' + "\n" 750 751 self.data += ".SH NAME\n" 752 self.data += f"{struct_type} {struct_name} \\- {purpose}\n" 753 754 # Replace tabs with two spaces and handle newlines 755 declaration = definition.replace("\t", " ") 756 declaration = KernRe(r"\n").sub('"\n.br\n.BI "', declaration) 757 758 self.data += ".SH SYNOPSIS\n" 759 self.data += f"{struct_type} {struct_name} " + "{" + "\n.br\n" 760 self.data += f'.BI "{declaration}\n' + "};\n.br\n\n" 761 762 self.data += ".SH Members\n" 763 for parameter in parameterlist: 764 if parameter.startswith("#"): 765 continue 766 767 parameter_name = re.sub(r"\[.*", "", parameter) 768 769 if parameterdescs.get(parameter_name) == KernelDoc.undescribed: 770 continue 771 772 self.data += f'.IP "{parameter}" 12' + "\n" 773 self.output_highlight(parameterdescs.get(parameter_name)) 774 775 for section, text in args.sections.items(): 776 self.data += f'.SH "{section}"' + "\n" 777 self.output_highlight(text) 778