• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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