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