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