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