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