1#!/usr/bin/python3 2# 3# Copyright 2016-2024 The Khronos Group Inc. 4# 5# SPDX-License-Identifier: Apache-2.0 6 7# genRef.py - create API ref pages from spec source files 8# 9# Usage: genRef.py files 10 11import argparse 12import io 13import os 14import re 15import sys 16from collections import OrderedDict 17from reflib import (findRefs, fixupRefs, loadFile, logDiag, logWarn, logErr, 18 printPageInfo, setLogFile) 19from reg import Registry 20from generator import GeneratorOptions 21from parse_dependency import dependencyNames 22from apiconventions import APIConventions 23 24 25# refpage 'type' attributes which are API entities and contain structured 26# content such as API includes, valid usage blocks, etc. 27refpage_api_types = ( 28 'basetypes', 29 'consts', 30 'defines', 31 'enums', 32 'flags', 33 'funcpointers', 34 'handles', 35 'protos', 36 'structs', 37) 38 39# Other refpage types - SPIR-V builtins, API feature blocks, etc. - which do 40# not have structured content. 41refpage_other_types = ( 42 'builtins', 43 'feature', 44 'freeform', 45 'spirv' 46) 47 48 49def makeExtensionInclude(name): 50 """Return an include command for a generated extension interface. 51 - name - extension name""" 52 53 return 'include::{}/meta/refpage.{}{}[]'.format( 54 conventions.generated_include_path, 55 name, 56 conventions.file_suffix) 57 58 59def makeAPIInclude(type, name): 60 """Return an include command for a generated API interface 61 - type - type of the API, e.g. 'flags', 'handles', etc 62 - name - name of the API""" 63 64 return 'include::{}/api/{}/{}{}\n'.format( 65 conventions.generated_include_path, 66 type, name, conventions.file_suffix) 67 68 69def isextension(name): 70 """Return True if name is an API extension name (ends with an upper-case 71 author ID). 72 73 This assumes that author IDs are at least two characters.""" 74 return name[-2:].isalpha() and name[-2:].isupper() 75 76 77def printCopyrightSourceComments(fp): 78 """Print Khronos CC-BY copyright notice on open file fp. 79 80 Writes an asciidoc comment block, which copyrights the source 81 file.""" 82 print('// Copyright 2014-2024 The Khronos Group Inc.', file=fp) 83 print('//', file=fp) 84 # This works around constraints of the 'reuse' tool 85 print('// SPDX' + '-License-Identifier: CC-BY-4.0', file=fp) 86 print('', file=fp) 87 88 89def printFooter(fp, leveloffset=0): 90 """Print footer material at the end of each refpage on open file fp. 91 92 If generating separate refpages, adds the copyright. 93 If generating the single combined refpage, just add a separator. 94 95 - leveloffset - number of levels to bias section titles up or down.""" 96 97 # Generate the section header. 98 # Default depth is 2. 99 depth = max(0, leveloffset + 2) 100 prefix = '=' * depth 101 102 print('ifdef::doctype-manpage[]', 103 f'{prefix} Copyright', 104 '', 105 'include::{config}/copyright-ccby' + conventions.file_suffix + '[]', 106 'endif::doctype-manpage[]', 107 '', 108 'ifndef::doctype-manpage[]', 109 '<<<', 110 'endif::doctype-manpage[]', 111 '', 112 sep='\n', file=fp) 113 114 115def macroPrefix(name): 116 """Add a spec asciidoc macro prefix to an API name, depending on its type 117 (protos, structs, enums, etc.). 118 119 If the name is not recognized, use the generic link macro 'reflink:'.""" 120 if name in api.basetypes: 121 return 'basetype:' + name 122 if name in api.defines: 123 return 'dlink:' + name 124 if name in api.enums: 125 return 'elink:' + name 126 if name in api.flags: 127 return 'tlink:' + name 128 if name in api.funcpointers: 129 return 'tlink:' + name 130 if name in api.handles: 131 return 'slink:' + name 132 if name in api.protos: 133 return 'flink:' + name 134 if name in api.structs: 135 return 'slink:' + name 136 if name == 'TBD': 137 return 'No cross-references are available' 138 return 'reflink:' + name 139 140 141def seeAlsoList(apiName, explicitRefs=None, apiAliases=[]): 142 """Return an asciidoc string with a list of 'See Also' references for the 143 API entity 'apiName', based on the relationship mapping in the api module. 144 145 'explicitRefs' is a list of additional cross-references. 146 147 If apiAliases is not None, it is a list of aliases of apiName whose 148 cross-references will also be included. 149 150 If no relationships are available, return None.""" 151 152 refs = set(()) 153 154 # apiName and its aliases are treated equally 155 allApis = apiAliases.copy() 156 allApis.append(apiName) 157 158 # Add all the implicit references to refs 159 for name in allApis: 160 if name in api.mapDict: 161 refs.update(api.mapDict[name]) 162 163 # Add all the explicit references 164 if explicitRefs is not None: 165 if isinstance(explicitRefs, str): 166 explicitRefs = explicitRefs.split() 167 refs.update(name for name in explicitRefs) 168 169 # Add extensions / core versions based on dependencies 170 for name in allApis: 171 if name in api.requiredBy: 172 for (base,dependency) in api.requiredBy[name]: 173 refs.add(base) 174 if dependency is not None: 175 # 'dependency' may be a boolean expression of extension 176 # names. 177 # Extract them for use in cross-references. 178 for extname in dependencyNames(dependency): 179 refs.add(extname) 180 181 if len(refs) == 0: 182 return None 183 else: 184 return ', '.join(macroPrefix(name) for name in sorted(refs)) + '\n' 185 186 187def remapIncludes(lines, baseDir, specDir): 188 """Remap include directives in a list of lines so they can be extracted to a 189 different directory. 190 191 Returns remapped lines. 192 193 - lines - text to remap 194 - baseDir - target directory 195 - specDir - source directory""" 196 # This should be compiled only once 197 includePat = re.compile(r'^include::(?P<path>.*)\[\]') 198 199 newLines = [] 200 for line in lines: 201 matches = includePat.search(line) 202 if matches is not None: 203 path = matches.group('path') 204 205 if path[0] != '{': 206 # Relative path to include file from here 207 incPath = specDir + '/' + path 208 # Remap to be relative to baseDir 209 newPath = os.path.relpath(incPath, baseDir) 210 newLine = 'include::' + newPath + '[]\n' 211 logDiag('remapIncludes: remapping', line, '->', newLine) 212 newLines.append(newLine) 213 else: 214 # An asciidoctor variable starts the path. 215 # This must be an absolute path, not needing to be rewritten. 216 newLines.append(line) 217 else: 218 newLines.append(line) 219 return newLines 220 221 222def refPageShell(pageName, pageDesc, fp, head_content = None, sections=None, tail_content=None, man_section=3): 223 """Generate body of a reference page. 224 225 - pageName - string name of the page 226 - pageDesc - string short description of the page 227 - fp - file to write to 228 - head_content - text to include before the sections 229 - sections - iterable returning (title,body) for each section. 230 - tail_content - text to include after the sections 231 - man_section - Unix man page section""" 232 233 printCopyrightSourceComments(fp) 234 235 print(':data-uri:', 236 ':icons: font', 237 ':attribute-missing: warn', 238 conventions.extra_refpage_headers, 239 '', 240 sep='\n', file=fp) 241 242 s = '{}({})'.format(pageName, man_section) 243 print('= ' + s, 244 '', 245 conventions.extra_refpage_body, 246 '', 247 sep='\n', file=fp) 248 if pageDesc.strip() == '': 249 pageDesc = 'NO SHORT DESCRIPTION PROVIDED' 250 logWarn('refPageHead: no short description provided for', pageName) 251 252 print('== Name', 253 '{} - {}'.format(pageName, pageDesc), 254 '', 255 sep='\n', file=fp) 256 257 if head_content is not None: 258 print(head_content, 259 '', 260 sep='\n', file=fp) 261 262 if sections is not None: 263 for title, content in sections.items(): 264 print('== {}'.format(title), 265 '', 266 content, 267 '', 268 sep='\n', file=fp) 269 270 if tail_content is not None: 271 print(tail_content, 272 '', 273 sep='\n', file=fp) 274 275 276def refPageHead(pageName, pageDesc, specText, fieldName, fieldText, descText, fp): 277 """Generate header of a reference page. 278 279 - pageName - string name of the page 280 - pageDesc - string short description of the page 281 - specType - string containing 'spec' field from refpage open block, or None. 282 Used to determine containing spec name and URL. 283 - specText - string that goes in the "C Specification" section 284 - fieldName - string heading an additional section following specText, if not None 285 - fieldText - string that goes in the additional section 286 - descText - string that goes in the "Description" section 287 - fp - file to write to""" 288 sections = OrderedDict() 289 290 if specText is not None: 291 sections['C Specification'] = specText 292 293 if fieldName is not None: 294 sections[fieldName] = fieldText 295 296 if descText is None or descText.strip() == '': 297 logWarn('refPageHead: no description provided for', pageName) 298 299 if descText is not None: 300 sections['Description'] = descText 301 302 refPageShell(pageName, pageDesc, fp, head_content=None, sections=sections) 303 304 305def refPageTail(pageName, 306 specType=None, 307 specAnchor=None, 308 seeAlso=None, 309 fp=None, 310 auto=False, 311 leveloffset=0): 312 """Generate end boilerplate of a reference page. 313 314 - pageName - name of the page 315 - specType - None or the 'spec' attribute from the refpage block, 316 identifying the specification name and URL this refpage links to. 317 - specAnchor - None or the 'anchor' attribute from the refpage block, 318 identifying the anchor in the specification this refpage links to. If 319 None, the pageName is assumed to be a valid anchor. 320 - seeAlso - text of the "See Also" section 321 - fp - file to write the page to 322 - auto - True if this is an entirely generated refpage, False if it is 323 handwritten content from the spec. 324 - leveloffset - number of levels to bias section titles up or down.""" 325 326 specName = conventions.api_name(specType) 327 specURL = conventions.specURL(specType) 328 if specAnchor is None: 329 specAnchor = pageName 330 331 if seeAlso is None: 332 seeAlso = 'No cross-references are available\n' 333 334 notes = [ 335 'For more information, see the {}#{}[{} Specification^]'.format( 336 specURL, specAnchor, specName), 337 '', 338 ] 339 340 if auto: 341 notes.extend(( 342 'This page is a generated document.', 343 'Fixes and changes should be made to the generator scripts, ' 344 'not directly.', 345 )) 346 else: 347 notes.extend(( 348 'This page is extracted from the ' + specName + ' Specification. ', 349 'Fixes and changes should be made to the Specification, ' 350 'not directly.', 351 )) 352 353 # Generate the section header. 354 # Default depth is 2. 355 depth = max(0, leveloffset + 2) 356 prefix = '=' * depth 357 358 print(f'{prefix} See Also', 359 '', 360 seeAlso, 361 '', 362 sep='\n', file=fp) 363 364 print(f'{prefix} Document Notes', 365 '', 366 '\n'.join(notes), 367 '', 368 sep='\n', file=fp) 369 370 printFooter(fp, leveloffset) 371 372 373def xrefRewriteInitialize(): 374 """Initialize substitution patterns for asciidoctor xrefs.""" 375 376 global refLinkPattern, refLinkSubstitute 377 global refLinkTextPattern, refLinkTextSubstitute 378 global specLinkPattern, specLinkSubstitute 379 380 # These are xrefs to API entities, rewritten to link to refpages 381 # The refLink variants are for xrefs with only an anchor and no text. 382 # The refLinkText variants are for xrefs with both anchor and text 383 refLinkPattern = re.compile(r'<<([Vv][Kk][A-Za-z0-9_]+)>>') 384 refLinkSubstitute = r'link:\1.html[\1^]' 385 386 refLinkTextPattern = re.compile(r'<<([Vv][Kk][A-Za-z0-9_]+)[,]?[ \t\n]*([^>,]*)>>') 387 refLinkTextSubstitute = r'link:\1.html[\2^]' 388 389 # These are xrefs to other anchors, rewritten to link to the spec 390 specLinkPattern = re.compile(r'<<([-A-Za-z0-9_.(){}:]+)[,]?[ \t\n]*([^>,]*)>>') 391 392 # Unfortunately, specLinkSubstitute depends on the link target, 393 # so cannot be constructed in advance. 394 specLinkSubstitute = None 395 396 397def xrefRewrite(text, specURL): 398 """Rewrite asciidoctor xrefs in text to resolve properly in refpages. 399 Xrefs which are to refpages are rewritten to link to those 400 refpages. The remainder are rewritten to generate external links into 401 the supplied specification document URL. 402 403 - text - string to rewrite, or None 404 - specURL - URL to target 405 406 Returns rewritten text, or None, respectively""" 407 408 global refLinkPattern, refLinkSubstitute 409 global refLinkTextPattern, refLinkTextSubstitute 410 global specLinkPattern, specLinkSubstitute 411 412 specLinkSubstitute = r'link:{}#\1[\2^]'.format(specURL) 413 414 if text is not None: 415 text, _ = refLinkPattern.subn(refLinkSubstitute, text) 416 text, _ = refLinkTextPattern.subn(refLinkTextSubstitute, text) 417 text, _ = specLinkPattern.subn(specLinkSubstitute, text) 418 419 return text 420 421def emitPage(baseDir, specDir, pi, file): 422 """Extract a single reference page into baseDir. 423 424 - baseDir - base directory to emit page into 425 - specDir - directory extracted page source came from 426 - pi - pageInfo for this page relative to file 427 - file - list of strings making up the file, indexed by pi""" 428 pageName = f'{baseDir}/{pi.name}{conventions.file_suffix}' 429 430 # Add a dictionary entry for this page 431 global genDict 432 genDict[pi.name] = None 433 logDiag('emitPage:', pageName) 434 435 # Short description 436 if pi.desc is None: 437 pi.desc = '(no short description available)' 438 439 # Member/parameter section label and text, if there is one 440 field = None 441 fieldText = None 442 443 # Only do structural checks on API pages 444 if pi.type in refpage_api_types: 445 if pi.include is None: 446 logWarn('emitPage:', pageName, 'INCLUDE is None, no page generated') 447 return 448 449 # Specification text from beginning to just before the parameter 450 # section. This covers the description, the prototype, the version 451 # note, and any additional version note text. If a parameter section 452 # is absent then go a line beyond the include. 453 remap_end = pi.include + 1 if pi.param is None else pi.param 454 lines = remapIncludes(file[pi.begin:remap_end], baseDir, specDir) 455 specText = ''.join(lines) 456 457 if pi.param is not None: 458 if pi.type == 'structs': 459 field = 'Members' 460 elif pi.type in ['protos', 'funcpointers']: 461 field = 'Parameters' 462 else: 463 logWarn('emitPage: unknown field type:', pi.type, 464 'for', pi.name) 465 lines = remapIncludes(file[pi.param:pi.body], baseDir, specDir) 466 fieldText = ''.join(lines) 467 468 # Description text 469 if pi.body != pi.include: 470 lines = remapIncludes(file[pi.body:pi.end + 1], baseDir, specDir) 471 descText = ''.join(lines) 472 else: 473 descText = None 474 logWarn('emitPage: INCLUDE == BODY, so description will be empty for', pi.name) 475 if pi.begin != pi.include: 476 logWarn('emitPage: Note: BEGIN != INCLUDE, so the description might be incorrectly located before the API include!') 477 elif pi.type in refpage_other_types: 478 specText = None 479 descText = ''.join(file[pi.begin:pi.end + 1]) 480 else: 481 # This should be caught in the spec markup checking tests 482 logErr(f"emitPage: refpage type='{pi.type}' is unrecognized") 483 484 # Rewrite asciidoctor xrefs to resolve properly in refpages 485 specURL = conventions.specURL(pi.spec) 486 487 specText = xrefRewrite(specText, specURL) 488 fieldText = xrefRewrite(fieldText, specURL) 489 descText = xrefRewrite(descText, specURL) 490 491 fp = open(pageName, 'w', encoding='utf-8') 492 refPageHead(pi.name, 493 pi.desc, 494 specText, 495 field, fieldText, 496 descText, 497 fp) 498 refPageTail(pageName=pi.name, 499 specType=pi.spec, 500 specAnchor=pi.anchor, 501 seeAlso=seeAlsoList(pi.name, pi.refs, pi.alias.split()), 502 fp=fp, 503 auto=False) 504 fp.close() 505 506 507def autoGenEnumsPage(baseDir, pi, file): 508 """Autogenerate a single reference page in baseDir. 509 510 Script only knows how to do this for /enums/ pages, at present. 511 512 - baseDir - base directory to emit page into 513 - pi - pageInfo for this page relative to file 514 - file - list of strings making up the file, indexed by pi""" 515 pageName = f'{baseDir}/{pi.name}{conventions.file_suffix}' 516 fp = open(pageName, 'w', encoding='utf-8') 517 518 # Add a dictionary entry for this page 519 global genDict 520 genDict[pi.name] = None 521 logDiag('autoGenEnumsPage:', pageName) 522 523 # Short description 524 if pi.desc is None: 525 pi.desc = '(no short description available)' 526 527 # Description text. Allow for the case where an enum definition 528 # is not embedded. 529 if not pi.embed: 530 embedRef = '' 531 else: 532 embedRef = ''.join(( 533 ' * The reference page for ', 534 macroPrefix(pi.embed), 535 ', where this interface is defined.\n')) 536 537 txt = ''.join(( 538 'For more information, see:\n\n', 539 embedRef, 540 ' * The See Also section for other reference pages using this type.\n', 541 ' * The ' + apiName + ' Specification.\n')) 542 543 refPageHead(pi.name, 544 pi.desc, 545 ''.join(file[pi.begin:pi.include + 1]), 546 None, None, 547 txt, 548 fp) 549 refPageTail(pageName=pi.name, 550 specType=pi.spec, 551 specAnchor=pi.anchor, 552 seeAlso=seeAlsoList(pi.name, pi.refs, pi.alias.split()), 553 fp=fp, 554 auto=True) 555 fp.close() 556 557 558# Pattern to break apart an API *Flags{authorID} name, used in 559# autoGenFlagsPage. 560flagNamePat = re.compile(r'(?P<name>\w+)Flags(?P<author>[A-Z]*)') 561 562 563def autoGenFlagsPage(baseDir, flagName): 564 """Autogenerate a single reference page in baseDir for an API *Flags type. 565 566 - baseDir - base directory to emit page into 567 - flagName - API *Flags name""" 568 pageName = f'{baseDir}/{flagName}{conventions.file_suffix}' 569 fp = open(pageName, 'w', encoding='utf-8') 570 571 # Add a dictionary entry for this page 572 global genDict 573 genDict[flagName] = None 574 logDiag('autoGenFlagsPage:', pageName) 575 576 # Short description 577 matches = flagNamePat.search(flagName) 578 if matches is not None: 579 name = matches.group('name') 580 author = matches.group('author') 581 logDiag('autoGenFlagsPage: split name into', name, 'Flags', author) 582 flagBits = name + 'FlagBits' + author 583 desc = 'Bitmask of ' + flagBits 584 else: 585 logWarn('autoGenFlagsPage:', pageName, 'does not end in "Flags{author ID}". Cannot infer FlagBits type.') 586 flagBits = None 587 desc = 'Unknown ' + apiName + ' flags type' 588 589 # Description text 590 if flagBits is not None: 591 txt = ''.join(( 592 'etext:' + flagName, 593 ' is a mask of zero or more elink:' + flagBits + '.\n', 594 'It is used as a member and/or parameter of the structures and commands\n', 595 'in the See Also section below.\n')) 596 else: 597 txt = ''.join(( 598 'etext:' + flagName, 599 ' is an unknown ' + apiName + ' type, assumed to be a bitmask.\n')) 600 601 refPageHead(flagName, 602 desc, 603 makeAPIInclude('flags', flagName), 604 None, None, 605 txt, 606 fp) 607 refPageTail(pageName=flagName, 608 specType=pi.spec, 609 specAnchor=pi.anchor, 610 seeAlso=seeAlsoList(flagName, None), 611 fp=fp, 612 auto=True) 613 fp.close() 614 615 616def autoGenHandlePage(baseDir, handleName): 617 """Autogenerate a single handle page in baseDir for an API handle type. 618 619 - baseDir - base directory to emit page into 620 - handleName - API handle name""" 621 # @@ Need to determine creation function & add handles/ include for the 622 # @@ interface in generator.py. 623 pageName = f'{baseDir}/{handleName}{conventions.file_suffix}' 624 fp = open(pageName, 'w', encoding='utf-8') 625 626 # Add a dictionary entry for this page 627 global genDict 628 genDict[handleName] = None 629 logDiag('autoGenHandlePage:', pageName) 630 631 # Short description 632 desc = apiName + ' object handle' 633 634 descText = ''.join(( 635 'sname:' + handleName, 636 ' is an object handle type, referring to an object used\n', 637 'by the ' + apiName + ' implementation. These handles are created or allocated\n', 638 'by the @@ TBD @@ function, and used by other ' + apiName + ' structures\n', 639 'and commands in the See Also section below.\n')) 640 641 refPageHead(handleName, 642 desc, 643 makeAPIInclude('handles', handleName), 644 None, None, 645 descText, 646 fp) 647 refPageTail(pageName=handleName, 648 specType=pi.spec, 649 specAnchor=pi.anchor, 650 seeAlso=seeAlsoList(handleName, None), 651 fp=fp, 652 auto=True) 653 fp.close() 654 655 656def genRef(specFile, baseDir): 657 """Extract reference pages from a spec asciidoc source file. 658 659 - specFile - filename to extract from 660 - baseDir - output directory to generate page in""" 661 # We do not care the newline format used here. 662 file, _ = loadFile(specFile) 663 if file is None: 664 return 665 666 # Save the path to this file for later use in rewriting relative includes 667 specDir = os.path.dirname(os.path.abspath(specFile)) 668 669 pageMap = findRefs(file, specFile) 670 logDiag(specFile + ': found', len(pageMap.keys()), 'potential pages') 671 672 sys.stderr.flush() 673 674 # Fix up references in pageMap 675 fixupRefs(pageMap, specFile, file) 676 677 # Create each page, if possible 678 pages = {} 679 680 for name in sorted(pageMap): 681 pi = pageMap[name] 682 683 # Only generate the page if it is in the requested build 684 # 'freeform' pages are always generated 685 # 'feature' pages (core versions & extensions) are generated if they are in 686 # the requested feature list 687 # All other pages (APIs) are generated if they are in the API map for 688 # the build. 689 if pi.type in refpage_api_types: 690 if name not in api.typeCategory: 691 # Also check aliases of name - api.nonexistent is the same 692 # mapping used to rewrite *link: macros in this build. 693 if name not in api.nonexistent: 694 logWarn(f'genRef: NOT generating feature page {name} - API not in this build') 695 continue 696 else: 697 logWarn(f'genRef: generating feature page {name} because its alias {api.nonexistent[name]} exists') 698 elif pi.type in refpage_other_types: 699 # The only non-API type which can be checked is a feature refpage 700 if pi.type == 'feature': 701 if name not in api.features: 702 logWarn(f'genRef: NOT generating feature page {name} - feature not in this build') 703 continue 704 705 printPageInfo(pi, file) 706 707 if pi.Warning: 708 logDiag('genRef:', pi.name + ':', pi.Warning) 709 710 if pi.extractPage: 711 emitPage(baseDir, specDir, pi, file) 712 elif pi.type == 'enums': 713 autoGenEnumsPage(baseDir, pi, file) 714 elif pi.type == 'flags': 715 autoGenFlagsPage(baseDir, pi.name) 716 else: 717 # Do not extract this page 718 logWarn('genRef: Cannot extract or autogenerate:', pi.name) 719 720 pages[pi.name] = pi 721 for alias in pi.alias.split(): 722 pages[alias] = pi 723 724 return pages 725 726 727def genSinglePageRef(baseDir): 728 """Generate the single-page version of the ref pages. 729 730 This assumes there is a page for everything in the api module dictionaries. 731 Extensions (KHR, EXT, etc.) are currently skipped""" 732 # Accumulate head of page 733 head = io.StringIO() 734 735 printCopyrightSourceComments(head) 736 737 print('= ' + apiName + ' API Reference Pages', 738 ':data-uri:', 739 ':icons: font', 740 ':doctype: book', 741 ':numbered!:', 742 ':max-width: 200', 743 ':data-uri:', 744 ':toc2:', 745 ':toclevels: 2', 746 ':attribute-missing: warn', 747 '', 748 sep='\n', file=head) 749 750 print('== Copyright', file=head) 751 print('', file=head) 752 print('include::{config}/copyright-ccby' + conventions.file_suffix + '[]', file=head) 753 print('', file=head) 754 755 # Inject the table of contents. Asciidoc really ought to be generating 756 # this for us. 757 758 sections = [ 759 [api.protos, 'protos', apiName + ' Commands'], 760 [api.handles, 'handles', 'Object Handles'], 761 [api.structs, 'structs', 'Structures'], 762 [api.enums, 'enums', 'Enumerations'], 763 [api.flags, 'flags', 'Flags'], 764 [api.funcpointers, 'funcpointers', 'Function Pointer Types'], 765 [api.basetypes, 'basetypes', apiName + ' Scalar types'], 766 [api.defines, 'defines', 'C Macro Definitions'], 767 [extensions, 'extensions', apiName + ' Extensions'] 768 ] 769 770 # Accumulate body of page 771 body = io.StringIO() 772 773 for (apiDict, label, title) in sections: 774 # Add section title/anchor header to body 775 anchor = '[[' + label + ',' + title + ']]' 776 print(anchor, 777 '== ' + title, 778 '', 779 ':leveloffset: 2', 780 '', 781 sep='\n', file=body) 782 783 if label == 'extensions': 784 # preserve order of extensions since we already sorted the way we want. 785 keys = apiDict.keys() 786 else: 787 keys = sorted(apiDict.keys()) 788 789 for refPage in keys: 790 # Do not generate links for aliases, which are included with the 791 # aliased page 792 if refPage not in api.alias: 793 # Add page to body 794 if 'FlagBits' in refPage and conventions.unified_flag_refpages: 795 # OpenXR does not create separate ref pages for FlagBits: 796 # the FlagBits includes go in the Flags refpage. 797 # Previously the Vulkan script would only emit non-empty 798 # Vk*Flags pages, via the logic 799 # if refPage not in api.flags or api.flags[refPage] is not None 800 # emit page 801 # Now, all are emitted. 802 continue 803 else: 804 print(f'include::{refPage}{conventions.file_suffix}[]', file=body) 805 else: 806 # Alternatively, we could (probably should) link to the 807 # aliased refpage 808 logWarn('(Benign) Not including', refPage, 809 'in single-page reference', 810 'because it is an alias of', api.alias[refPage]) 811 812 print('\n' + ':leveloffset: 0' + '\n', file=body) 813 814 # Write head and body to the output file 815 pageName = f'{baseDir}/apispec{conventions.file_suffix}' 816 fp = open(pageName, 'w', encoding='utf-8') 817 818 print(head.getvalue(), file=fp, end='') 819 print(body.getvalue(), file=fp, end='') 820 821 head.close() 822 body.close() 823 fp.close() 824 825 826def genExtension(baseDir, extpath, name, info): 827 """Generate refpage, and add dictionary entry for an extension 828 829 - baseDir - output directory to generate page in 830 - extpath - None, or path to per-extension specification sources if 831 those are to be included in extension refpages 832 - name - extension name 833 - info - <extension> Element from XML""" 834 835 # Add a dictionary entry for this page 836 global genDict 837 genDict[name] = None 838 declares = [] 839 elem = info.elem 840 841 # Type of extension (instance, device, etc.) 842 ext_type = elem.get('type') 843 844 # Autogenerate interfaces from <extension> entry 845 for required in elem.find('require'): 846 req_name = required.get('name') 847 if not req_name: 848 # This is not what we are looking for 849 continue 850 if req_name.endswith('_SPEC_VERSION') or req_name.endswith('_EXTENSION_NAME'): 851 # Do not link to spec version or extension name - those ref pages are not created. 852 continue 853 854 if required.get('extends'): 855 # These are either extensions of enumerated types, or const enum 856 # values: neither of which get a ref page - although we could 857 # include the enumerated types in the See Also list. 858 continue 859 860 if req_name not in genDict: 861 if req_name in api.alias: 862 logWarn(f'WARN: {req_name} (in extension {name}) is an alias, so does not have a ref page') 863 else: 864 logWarn(f'ERROR: {req_name} (in extension {name}) does not have a ref page.') 865 866 declares.append(req_name) 867 868 appbody = None 869 tail_content = None 870 if extpath is not None: 871 try: 872 appPath = extpath + '/' + conventions.extension_file_path(name) 873 appfp = open(appPath, 'r', encoding='utf-8') 874 appbody = appfp.read() 875 appfp.close() 876 877 # Transform internal links to crosslinks 878 specURL = conventions.specURL() 879 appbody = xrefRewrite(appbody, specURL) 880 except FileNotFoundError: 881 print('Cannot find extension appendix for', name) 882 logWarn('Cannot find extension appendix for', name) 883 884 # Fall through to autogenerated page 885 extpath = None 886 appbody = None 887 888 appbody = f'Cannot find extension appendix {appPath} for {name}\n' 889 else: 890 tail_content = makeExtensionInclude(name) 891 892 # Write the extension refpage 893 pageName = f'{baseDir}/{name}{conventions.file_suffix}' 894 logDiag('genExtension:', pageName) 895 fp = open(pageName, 'w', encoding='utf-8') 896 897 # There are no generated titled sections 898 sections = None 899 900 refPageShell(name, 901 "{} extension".format(ext_type), 902 fp, 903 appbody, 904 sections=sections, 905 tail_content=tail_content) 906 907 # Restore leveloffset for boilerplate in refPageTail 908 if conventions.include_extension_appendix_in_refpage: 909 # The generated metadata include (refpage.extensionname.adoc) moved 910 # the leveloffset attribute by -1 to account for the relative 911 # structuring of the spec extension appendix section structure vs. 912 # the refpages. 913 # This restores leveloffset for the boilerplate in refPageTail. 914 leveloffset = 1 915 else: 916 leveloffset = 0 917 918 refPageTail(pageName=name, 919 specType=None, 920 specAnchor=name, 921 seeAlso=seeAlsoList(name, declares), 922 fp=fp, 923 auto=True, 924 leveloffset=leveloffset) 925 fp.close() 926 927 928if __name__ == '__main__': 929 global genDict, extensions, conventions, apiName 930 genDict = {} 931 extensions = OrderedDict() 932 conventions = APIConventions() 933 apiName = conventions.api_name('api') 934 935 parser = argparse.ArgumentParser() 936 937 parser.add_argument('-diag', action='store', dest='diagFile', 938 help='Set the diagnostic file') 939 parser.add_argument('-warn', action='store', dest='warnFile', 940 help='Set the warning file') 941 parser.add_argument('-log', action='store', dest='logFile', 942 help='Set the log file for both diagnostics and warnings') 943 parser.add_argument('-genpath', action='store', 944 default='gen', 945 help='Path to directory containing generated files') 946 parser.add_argument('-basedir', action='store', dest='baseDir', 947 default=None, 948 help='Set the base directory in which pages are generated') 949 parser.add_argument('-noauto', action='store_true', 950 help='Don\'t generate inferred ref pages automatically') 951 parser.add_argument('files', metavar='filename', nargs='*', 952 help='a filename to extract ref pages from') 953 parser.add_argument('--version', action='version', version='%(prog)s 1.0') 954 parser.add_argument('-extension', action='append', 955 default=[], 956 help='Specify an extension or extensions to add to targets') 957 parser.add_argument('-rewrite', action='store', 958 default=None, 959 help='Name of output file to write Apache mod_rewrite directives to') 960 parser.add_argument('-toc', action='store', 961 default=None, 962 help='Name of output file to write an alphabetical TOC to') 963 parser.add_argument('-registry', action='store', 964 default=conventions.registry_path, 965 help='Use specified registry file instead of default') 966 parser.add_argument('-extpath', action='store', 967 default=None, 968 help='Use extension descriptions from this directory instead of autogenerating extension refpages') 969 970 results = parser.parse_args() 971 972 # Load the generated apimap module 973 sys.path.insert(0, results.genpath) 974 import apimap as api 975 976 setLogFile(True, True, results.logFile) 977 setLogFile(True, False, results.diagFile) 978 setLogFile(False, True, results.warnFile) 979 980 # Initialize static rewrite patterns for spec xrefs 981 xrefRewriteInitialize() 982 983 if results.baseDir is None: 984 baseDir = results.genpath + '/ref' 985 else: 986 baseDir = results.baseDir 987 988 # Dictionary of pages & aliases 989 pages = {} 990 991 for file in results.files: 992 d = genRef(file, baseDir) 993 pages.update(d) 994 995 # Now figure out which pages were not generated from the spec. 996 # This relies on the dictionaries of API constructs in the api module. 997 998 if not results.noauto: 999 # Must have an apiname selected to avoid complaints from 1000 # registry.loadFile, even though it is irrelevant to our uses. 1001 genOpts = GeneratorOptions(apiname = conventions.xml_api_name) 1002 registry = Registry(genOpts = genOpts) 1003 registry.loadFile(results.registry) 1004 1005 if conventions.write_refpage_include: 1006 # Only extensions with a supported="..." attribute in this set 1007 # will be considered for extraction/generation. 1008 ext_names = set(k for k, v in registry.extdict.items() 1009 if conventions.xml_api_name in v.supported.split(',')) 1010 1011 desired_extensions = ext_names.intersection(set(results.extension)) 1012 for prefix in conventions.extension_index_prefixes: 1013 # Splits up into chunks, sorted within each chunk. 1014 filtered_extensions = sorted( 1015 [name for name in desired_extensions 1016 if name.startswith(prefix) and name not in extensions]) 1017 for name in filtered_extensions: 1018 # logWarn('NOT autogenerating extension refpage for', name) 1019 extensions[name] = None 1020 genExtension(baseDir, results.extpath, name, registry.extdict[name]) 1021 1022 # autoGenFlagsPage is no longer needed because they are added to 1023 # the spec sources now. 1024 # for page in api.flags: 1025 # if page not in genDict: 1026 # autoGenFlagsPage(baseDir, page) 1027 1028 # autoGenHandlePage is no longer needed because they are added to 1029 # the spec sources now. 1030 # for page in api.structs: 1031 # if typeCategory[page] == 'handle': 1032 # autoGenHandlePage(baseDir, page) 1033 1034 sections = [ 1035 (api.flags, 'Flag Types'), 1036 (api.enums, 'Enumerated Types'), 1037 (api.structs, 'Structures'), 1038 (api.protos, 'Prototypes'), 1039 (api.funcpointers, 'Function Pointers'), 1040 (api.basetypes, apiName + ' Scalar Types'), 1041 (extensions, apiName + ' Extensions'), 1042 ] 1043 1044 # Summarize pages that were not generated, for good or bad reasons 1045 1046 for (apiDict, title) in sections: 1047 # OpenXR was keeping a 'flagged' state which only printed out a 1048 # warning for the first non-generated page, but was otherwise 1049 # unused. This does not seem helpful. 1050 for page in apiDict: 1051 if page not in genDict: 1052 # Page was not generated - why not? 1053 if page in api.alias: 1054 logDiag('(Benign, is an alias) Ref page for', title, page, 'is aliased into', api.alias[page]) 1055 elif page in api.flags and api.flags[page] is None: 1056 logDiag('(Benign, no FlagBits defined) No ref page generated for ', title, 1057 page) 1058 else: 1059 # Could introduce additional logic to detect 1060 # external types and not emit them. 1061 logWarn('No ref page generated for ', title, page) 1062 1063 genSinglePageRef(baseDir) 1064 1065 if results.rewrite: 1066 # Generate Apache rewrite directives for refpage aliases 1067 fp = open(results.rewrite, 'w', encoding='utf-8') 1068 1069 for page in sorted(pages): 1070 p = pages[page] 1071 rewrite = p.name 1072 1073 if page != rewrite: 1074 print('RewriteRule ^', page, '.html$ ', rewrite, '.html', 1075 sep='', file=fp) 1076 fp.close() 1077 1078 if results.toc: 1079 # Generate dynamic portion of refpage TOC 1080 fp = open(results.toc, 'w', encoding='utf-8') 1081 1082 # Run through dictionary of pages generating an TOC 1083 print(12 * ' ', '<li class="Level1">Alphabetic Contents', sep='', file=fp) 1084 print(16 * ' ', '<ul class="Level2">', sep='', file=fp) 1085 lastLetter = None 1086 1087 for page in sorted(pages, key=str.upper): 1088 p = pages[page] 1089 letter = page[0:1].upper() 1090 1091 if letter != lastLetter: 1092 if lastLetter: 1093 # End previous block 1094 print(24 * ' ', '</ul>', sep='', file=fp) 1095 print(20 * ' ', '</li>', sep='', file=fp) 1096 # Start new block 1097 print(20 * ' ', '<li>', letter, sep='', file=fp) 1098 print(24 * ' ', '<ul class="Level3">', sep='', file=fp) 1099 lastLetter = letter 1100 1101 # Add this page to the list 1102 print(28 * ' ', '<li><a href="', p.name, '.html" ', 1103 'target="pagedisplay">', page, '</a></li>', 1104 sep='', file=fp) 1105 1106 if lastLetter: 1107 # Close the final letter block 1108 print(24 * ' ', '</ul>', sep='', file=fp) 1109 print(20 * ' ', '</li>', sep='', file=fp) 1110 1111 # Close the list 1112 print(16 * ' ', '</ul>', sep='', file=fp) 1113 print(12 * ' ', '</li>', sep='', file=fp) 1114 1115 # print('name {} -> page {}'.format(page, pages[page].name)) 1116 1117 fp.close() 1118