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