• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/python3 -i
2#
3# Copyright 2013-2023 The Khronos Group Inc.
4#
5# SPDX-License-Identifier: Apache-2.0
6
7import os
8import re
9import sys
10from functools import total_ordering
11from generator import GeneratorOptions, OutputGenerator, regSortFeatures, write
12from parse_dependency import dependencyMarkup
13
14class ExtensionMetaDocGeneratorOptions(GeneratorOptions):
15    """ExtensionMetaDocGeneratorOptions - subclass of GeneratorOptions.
16
17    Represents options during extension metainformation generation for Asciidoc"""
18    def __init__(self, *args, **kwargs):
19        super().__init__(*args, **kwargs)
20
21@total_ordering
22class Extension:
23    def __init__(self,
24                 generator, # needed for logging and API conventions
25                 filename,
26                 name,
27                 number,
28                 ext_type,
29                 depends,
30                 contact,
31                 promotedTo,
32                 deprecatedBy,
33                 obsoletedBy,
34                 provisional,
35                 revision,
36                 specialuse,
37                 ratified
38                ):
39        self.generator = generator
40        self.conventions = generator.genOpts.conventions
41        self.filename = filename
42        self.name = name
43        self.number = number
44        self.ext_type = ext_type
45        self.depends = depends
46        self.contact = contact
47        self.promotedTo = promotedTo
48        self.deprecatedBy = deprecatedBy
49        self.obsoletedBy = obsoletedBy
50        self.provisional = provisional
51        self.revision = revision
52        self.specialuse = specialuse
53        self.ratified = ratified
54
55        self.deprecationType = None
56        self.supercedingAPIVersion = None
57        self.supercedingExtension = None
58        # This is a set containing names of extensions (if any) promoted
59        # *to* this extension.
60        # It is filled in after all the Extension objects are created,
61        # since it requires a reverse mapping step.
62        self.promotedFrom = set()
63
64        if self.promotedTo is not None and self.deprecatedBy is not None and self.obsoletedBy is not None:
65            self.generator.logMsg('warn', 'All \'promotedto\', \'deprecatedby\' and \'obsoletedby\' attributes used on extension ' + self.name + '! Ignoring \'promotedto\' and \'deprecatedby\'.')
66        elif self.promotedTo is not None and self.deprecatedBy is not None:
67            self.generator.logMsg('warn', 'Both \'promotedto\' and \'deprecatedby\' attributes used on extension ' + self.name + '! Ignoring \'deprecatedby\'.')
68        elif self.promotedTo is not None and self.obsoletedBy is not None:
69            self.generator.logMsg('warn', 'Both \'promotedto\' and \'obsoletedby\' attributes used on extension ' + self.name + '! Ignoring \'promotedto\'.')
70        elif self.deprecatedBy is not None and self.obsoletedBy is not None:
71            self.generator.logMsg('warn', 'Both \'deprecatedby\' and \'obsoletedby\' attributes used on extension ' + self.name + '! Ignoring \'deprecatedby\'.')
72
73        supercededBy = None
74        if self.promotedTo is not None:
75            self.deprecationType = 'promotion'
76            supercededBy = promotedTo
77        elif self.deprecatedBy is not None:
78            self.deprecationType = 'deprecation'
79            supercededBy = deprecatedBy
80        elif self.obsoletedBy is not None:
81            self.deprecationType = 'obsoletion'
82            supercededBy = obsoletedBy
83
84        if supercededBy is not None:
85            if supercededBy == '' and not self.deprecationType == 'promotion':
86                pass # supercedingAPIVersion, supercedingExtension is None
87            elif supercededBy.startswith(self.conventions.api_version_prefix):
88                self.supercedingAPIVersion = supercededBy
89            elif supercededBy.startswith(self.conventions.api_prefix):
90                self.supercedingExtension = supercededBy
91            else:
92                self.generator.logMsg('error', 'Unrecognized ' + self.deprecationType + ' attribute value \'' + supercededBy + '\'!')
93
94    def __str__(self):
95        return self.name
96    def __eq__(self, other):
97        return self.name == other.name
98    def __ne__(self, other):
99        return self.name != other.name
100
101    def __lt__(self, other):
102        self_is_KHR = self.name.startswith(self.conventions.KHR_prefix)
103        self_is_EXT = self.name.startswith(self.conventions.EXT_prefix)
104        other_is_KHR = other.name.startswith(self.conventions.KHR_prefix)
105        other_is_EXT = other.name.startswith(self.conventions.EXT_prefix)
106
107        swap = False
108        if self_is_KHR and not other_is_KHR:
109            return not swap
110        if other_is_KHR and not self_is_KHR:
111            return swap
112        if self_is_EXT and not other_is_EXT:
113            return not swap
114        if other_is_EXT and not self_is_EXT:
115            return swap
116
117        return self.name < other.name
118
119    def typeToStr(self):
120        if self.ext_type == 'instance':
121            return 'Instance extension'
122        if self.ext_type == 'device':
123            return 'Device extension'
124
125        if self.ext_type is not None:
126            self.generator.logMsg('warn', 'The type attribute of ' + self.name + ' extension is neither \'instance\' nor \'device\'. That is invalid (at the time this script was written).')
127        else: # should be unreachable
128            self.generator.logMsg('error', 'Logic error in typeToStr(): Missing type attribute!')
129        return None
130
131    def specLink(self, xrefName, xrefText, isRefpage = False):
132        """Generate a string containing a link to a specification anchor in
133           asciidoctor markup form.
134
135        - xrefName - anchor name in the spec
136        - xrefText - text to show for the link, or None
137        - isRefpage = True if generating a refpage include, False if
138          generating a specification extension appendix include"""
139
140        if isRefpage:
141            # Always link into API spec
142            specURL = self.conventions.specURL('api')
143            return 'link:{}#{}[{}^]'.format(specURL, xrefName, xrefText)
144        else:
145            return '<<' + xrefName + ', ' + xrefText + '>>'
146
147    def conditionalLinkCoreAPI(self, apiVersion, linkSuffix, isRefpage):
148        versionMatch = re.match(self.conventions.api_version_prefix + r'(\d+)_(\d+)', apiVersion)
149        major = versionMatch.group(1)
150        minor = versionMatch.group(2)
151
152        dottedVersion = major + '.' + minor
153
154        xrefName = 'versions-' + dottedVersion + linkSuffix
155        xrefText = self.conventions.api_name() + ' ' + dottedVersion
156
157        doc  = 'ifdef::' + apiVersion + '[]\n'
158        doc += '    ' + self.specLink(xrefName, xrefText, isRefpage) + '\n'
159        doc += 'endif::' + apiVersion + '[]\n'
160        doc += 'ifndef::' + apiVersion + '[]\n'
161        doc += '    ' + self.conventions.api_name() + ' ' + dottedVersion + '\n'
162        doc += 'endif::' + apiVersion + '[]\n'
163
164        return doc
165
166    def conditionalLinkExt(self, extName, indent = '    '):
167        doc  = 'ifdef::' + extName + '[]\n'
168        doc +=  indent + self.conventions.formatExtension(extName) + '\n'
169        doc += 'endif::' + extName + '[]\n'
170        doc += 'ifndef::' + extName + '[]\n'
171        doc += indent + '`' + extName + '`\n'
172        doc += 'endif::' + extName + '[]\n'
173
174        return doc
175
176    def resolveDeprecationChain(self, extensions, succeededBy, isRefpage, file):
177        if succeededBy not in extensions:
178            write(f'  ** *NOTE* The extension `{succeededBy}` is not supported for the API specification being generated', file=file)
179            self.generator.logMsg('warn', f'resolveDeprecationChain: {self.name} defines a superseding interface {succeededBy} which is not in the supported extensions list')
180            return
181
182        ext = extensions[succeededBy]
183
184        if ext.deprecationType:
185            if ext.deprecationType == 'promotion':
186                if ext.supercedingAPIVersion:
187                    write('  ** Which in turn was _promoted_ to\n' + ext.conditionalLinkCoreAPI(ext.supercedingAPIVersion, '-promotions', isRefpage), file=file)
188                else: # ext.supercedingExtension
189                    write('  ** Which in turn was _promoted_ to extension\n' + ext.conditionalLinkExt(ext.supercedingExtension), file=file)
190                    ext.resolveDeprecationChain(extensions, ext.supercedingExtension, file)
191            elif ext.deprecationType == 'deprecation':
192                if ext.supercedingAPIVersion:
193                    write('  ** Which in turn was _deprecated_ by\n' + ext.conditionalLinkCoreAPI(ext.supercedingAPIVersion, '-new-feature', isRefpage), file=file)
194                elif ext.supercedingExtension:
195                    write('  ** Which in turn was _deprecated_ by\n' + ext.conditionalLinkExt(ext.supercedingExtension) + '    extension', file=file)
196                    ext.resolveDeprecationChain(extensions, ext.supercedingExtension, file)
197                else:
198                    write('  ** Which in turn was _deprecated_ without replacement', file=file)
199            elif ext.deprecationType == 'obsoletion':
200                if ext.supercedingAPIVersion:
201                    write('  ** Which in turn was _obsoleted_ by\n' + ext.conditionalLinkCoreAPI(ext.supercedingAPIVersion, '-new-feature', isRefpage), file=file)
202                elif ext.supercedingExtension:
203                    write('  ** Which in turn was _obsoleted_ by\n' + ext.conditionalLinkExt(ext.supercedingExtension) + '    extension', file=file)
204                    ext.resolveDeprecationChain(extensions, ext.supercedingExtension, file)
205                else:
206                    write('  ** Which in turn was _obsoleted_ without replacement', file=file)
207            else: # should be unreachable
208                self.generator.logMsg('error', 'Logic error in resolveDeprecationChain(): deprecationType is neither \'promotion\', \'deprecation\' nor \'obsoletion\'!')
209
210
211    def writeTag(self, tag, value, isRefpage, fp):
212        """Write a tag and (if non-None) a tag value to a file.
213
214           If the value is None, just write the tag.
215
216           If the tag is None, just write the value (used for adding a value
217           to a just-written tag).
218
219        - tag - string tag name
220        - value - tag value, or None
221        - isRefpage - controls style in which the tag is marked up
222        - fp - open file pointer to write to"""
223
224        if isRefpage:
225            # Use subsection headers for the tag name
226            tagPrefix = '== '
227            tagSuffix = ''
228        else:
229            # Use an bolded item list for the tag name
230            tagPrefix = '*'
231            tagSuffix = '*::'
232
233        if tag is not None:
234            write(tagPrefix + tag + tagSuffix, file=fp)
235        if value is not None:
236            write(value, file=fp)
237
238        if isRefpage:
239            write('', file=fp)
240
241    def makeMetafile(self, extensions, isRefpage = False):
242        """Generate a file containing extension metainformation in
243           asciidoctor markup form.
244
245        - extensions - dictionary of Extension objects for extensions spec
246          is being generated against
247        - isRefpage - True if generating a refpage include, False if
248          generating a specification extension appendix include"""
249
250        if isRefpage:
251            filename = self.filename.replace('meta/', 'meta/refpage.')
252        else:
253            filename = self.filename
254
255        fp = self.generator.newFile(filename)
256
257        if not isRefpage:
258            write('[[' + self.name + ']]', file=fp)
259            write('=== ' + self.name, file=fp)
260            write('', file=fp)
261
262            self.writeTag('Name String', '`' + self.name + '`', isRefpage, fp)
263            self.writeTag('Extension Type', self.typeToStr(), isRefpage, fp)
264
265        self.writeTag('Registered Extension Number', self.number, isRefpage, fp)
266        self.writeTag('Revision', self.revision, isRefpage, fp)
267
268        if self.conventions.xml_api_name in self.ratified.split(','):
269            ratstatus = 'Ratified'
270        else:
271            ratstatus = 'Not ratified'
272        self.writeTag('Ratification Status', ratstatus, isRefpage, fp)
273
274        # Only API extension dependencies are coded in XML, others are explicit
275        self.writeTag('Extension and Version Dependencies', None, isRefpage, fp)
276
277        # Transform the boolean 'depends' expression into equivalent
278        # human-readable asciidoc markup.
279        if self.depends is not None:
280            if isRefpage:
281                separator = ''
282            else:
283                separator = '+'
284            write(separator + '\n--\n' +
285                  dependencyMarkup(self.depends) +
286                  '--', file=fp)
287        else:
288            # Do not bother specifying the base Vulkan 1.0 API redundantly
289            True
290
291        if self.provisional == 'true' and self.conventions.provisional_extension_warning:
292            write('  * *This is a _provisional_ extension and must: be used with caution.', file=fp)
293            write('    See the ' +
294                  self.specLink(xrefName = 'boilerplate-provisional-header',
295                                xrefText = 'description',
296                                isRefpage = isRefpage) +
297                  ' of provisional header files for enablement and stability details.*', file=fp)
298        write('', file=fp)
299
300        if self.deprecationType:
301            self.writeTag('Deprecation State', None, isRefpage, fp)
302
303            if self.deprecationType == 'promotion':
304                if self.supercedingAPIVersion:
305                    write('  * _Promoted_ to\n' + self.conditionalLinkCoreAPI(self.supercedingAPIVersion, '-promotions', isRefpage), file=fp)
306                else: # ext.supercedingExtension
307                    write('  * _Promoted_ to\n' + self.conditionalLinkExt(self.supercedingExtension) + '    extension', file=fp)
308                    self.resolveDeprecationChain(extensions, self.supercedingExtension, isRefpage, fp)
309            elif self.deprecationType == 'deprecation':
310                if self.supercedingAPIVersion:
311                    write('  * _Deprecated_ by\n' + self.conditionalLinkCoreAPI(self.supercedingAPIVersion, '-new-features', isRefpage), file=fp)
312                elif self.supercedingExtension:
313                    write('  * _Deprecated_ by\n' + self.conditionalLinkExt(self.supercedingExtension) + '    extension' , file=fp)
314                    self.resolveDeprecationChain(extensions, self.supercedingExtension, isRefpage, fp)
315                else:
316                    write('  * _Deprecated_ without replacement' , file=fp)
317            elif self.deprecationType == 'obsoletion':
318                if self.supercedingAPIVersion:
319                    write('  * _Obsoleted_ by\n' + self.conditionalLinkCoreAPI(self.supercedingAPIVersion, '-new-features', isRefpage), file=fp)
320                elif self.supercedingExtension:
321                    write('  * _Obsoleted_ by\n' + self.conditionalLinkExt(self.supercedingExtension) + '    extension' , file=fp)
322                    self.resolveDeprecationChain(extensions, self.supercedingExtension, isRefpage, fp)
323                else:
324                    # TODO: Does not make sense to retroactively ban use of extensions from 1.0.
325                    #       Needs some tweaks to the semantics and this message, when such extension(s) occur.
326                    write('  * _Obsoleted_ without replacement' , file=fp)
327            else: # should be unreachable
328                self.generator.logMsg('error', 'Logic error in makeMetafile(): deprecationType is neither \'promotion\', \'deprecation\' nor \'obsoletion\'!')
329            write('', file=fp)
330
331        if self.specialuse is not None:
332            specialuses = self.specialuse.split(',')
333            if len(specialuses) > 1:
334                header = 'Special Uses'
335            else:
336                header = 'Special Use'
337            self.writeTag(header, None, isRefpage, fp)
338
339            for use in specialuses:
340                # Each specialuse attribute value expands an asciidoctor
341                # attribute of the same name, instead of using the shorter,
342                # and harder to understand attribute
343                write('* {}'.format(
344                      self.specLink(
345                           xrefName = self.conventions.special_use_section_anchor,
346                           xrefText = '{' + use + '}',
347                           isRefpage = isRefpage)), file=fp)
348            write('', file=fp)
349
350        if self.conventions.write_contacts:
351            self.writeTag('Contact', None, isRefpage, fp)
352
353            contacts = self.contact.split(',')
354            for contact in contacts:
355                contactWords = contact.strip().split()
356                name = ' '.join(contactWords[:-1])
357                handle = contactWords[-1]
358                if handle.startswith('gitlab:'):
359                    prettyHandle = 'icon:gitlab[alt=GitLab, role="red"]' + handle.replace('gitlab:@', '')
360                elif handle.startswith('@'):
361                    issuePlaceholderText = '[' + self.name + '] ' + handle
362                    issuePlaceholderText += '%0A*Here describe the issue or question you have about the ' + self.name + ' extension*'
363                    trackerLink = 'link:++https://github.com/KhronosGroup/Vulkan-Docs/issues/new?body=' + issuePlaceholderText + '++'
364                    prettyHandle = trackerLink + '[icon:github[alt=GitHub,role="black"]' + handle[1:] + ',window=_blank,opts=nofollow]'
365                else:
366                    prettyHandle = handle
367
368                write('  * ' + name + ' ' + prettyHandle, file=fp)
369            write('', file=fp)
370
371        # Check if a proposal document for this extension exists in the
372        # current repository, and link to the same document (parameterized
373        # by a URL prefix attribute) if it does.
374        # The assumption is that a proposal document for an extension
375        # VK_name will be located in 'proposals/VK_name.adoc' relative
376        # to the repository root, and that this script will be invoked from
377        # the repository root.
378        # If a proposal for this extension does not exist, look for
379        # proposals for the extensions it is promoted from.
380
381        def checkProposal(extname):
382            """Check if a proposal document for an extension exists,
383               returning the path to that proposal or None otherwise."""
384
385            path = 'proposals/{}.adoc'.format(extname)
386            if os.path.exists(path) and os.access(path, os.R_OK):
387                return path
388            else:
389                return None
390
391        # List of [ extname, proposal link ]
392        proposals = []
393
394        path = checkProposal(self.name)
395        if path is not None:
396            proposals.append([self.name, path])
397        else:
398            for name in self.promotedFrom:
399                path = checkProposal(name)
400                if path is not None:
401                    proposals.append([name, path])
402
403        if len(proposals) > 0:
404            tag = 'Extension Proposal'
405            for (name, path) in sorted(proposals):
406                self.writeTag(tag,
407                    f'link:{{specRepositoryURL}}/{path}[{name}]',
408                    isRefpage, fp)
409                # Setting tag = None so additional values will not get
410                # additional tag headers.
411                tag = None
412
413        # If this is metadata to be included in a refpage, adjust the
414        # leveloffset to account for the relative structure of the extension
415        # appendices vs. refpages.
416        if isRefpage and self.conventions.include_extension_appendix_in_refpage:
417            write(':leveloffset: -1', file=fp)
418
419        fp.close()
420
421class ExtensionMetaDocOutputGenerator(OutputGenerator):
422    """ExtensionMetaDocOutputGenerator - subclass of OutputGenerator.
423
424    Generates AsciiDoc includes with metainformation for the API extension
425    appendices. The fields used from <extension> tags in the API XML are:
426
427    - name          extension name string
428    - number        extension number (optional)
429    - contact       name and GitHub login or email address (optional)
430    - type          'instance' | 'device' (optional)
431    - depends       boolean expression of core version and extension names this depends on (optional)
432    - promotedTo    extension or API version it was promoted to
433    - deprecatedBy  extension or API version which deprecated this extension,
434                    or empty string if deprecated without replacement
435    - obsoletedBy   extension or API version which obsoleted this extension,
436                    or empty string if obsoleted without replacement
437    - provisional   'true' if this extension is released provisionally"""
438
439    def __init__(self, *args, **kwargs):
440        super().__init__(*args, **kwargs)
441        self.extensions = {}
442        # List of strings containing all vendor tags
443        self.vendor_tags = []
444        self.file_suffix = ''
445
446    def newFile(self, filename):
447        self.logMsg('diag', '# Generating include file:', filename)
448        fp = open(filename, 'w', encoding='utf-8')
449        write(self.genOpts.conventions.warning_comment, file=fp)
450        return fp
451
452    def beginFile(self, genOpts):
453        OutputGenerator.beginFile(self, genOpts)
454
455        self.directory = self.genOpts.directory
456        self.file_suffix = self.genOpts.conventions.file_suffix
457
458        # Iterate over all 'tag' Elements and add the names of all the valid vendor
459        # tags to the list
460        root = self.registry.tree.getroot()
461        for tag in root.findall('tags/tag'):
462            self.vendor_tags.append(tag.get('name'))
463
464        # Create subdirectory, if needed
465        self.makeDir(self.directory)
466
467    def conditionalExt(self, extName, content, ifdef = None, condition = None):
468        doc = ''
469
470        innerdoc  = 'ifdef::' + extName + '[]\n'
471        innerdoc += content + '\n'
472        innerdoc += 'endif::' + extName + '[]\n'
473
474        if ifdef:
475            if ifdef == 'ifndef':
476                if condition:
477                    doc += 'ifndef::' + condition + '[]\n'
478                    doc += innerdoc
479                    doc += 'endif::' + condition + '[]\n'
480                else: # no condition is as if condition is defined; "nothing" is always defined :p
481                    pass # so no output
482            elif ifdef == 'ifdef':
483                if condition:
484                    doc += 'ifdef::' + condition + '+' + extName + '[]\n'
485                    doc += content + '\n' # does not include innerdoc; the ifdef was merged with the one above
486                    doc += 'endif::' + condition + '+' + extName + '[]\n'
487                else: # no condition is as if condition is defined; "nothing" is always defined :p
488                    doc += innerdoc
489            else: # should be unreachable
490                raise RuntimeError('Should be unreachable: ifdef is neither \'ifdef \' nor \'ifndef\'!')
491        else:
492            doc += innerdoc
493
494        return doc
495
496    def makeExtensionInclude(self, extname):
497        return self.conventions.extension_include_string(extname)
498
499    def endFile(self):
500        # Determine the extension an extension is promoted from, if any.
501        # This is used when attempting to locate a proposal document in
502        # makeMetafile() below.
503        for (extname, ext) in self.extensions.items():
504            promotedTo = ext.promotedTo
505            if promotedTo is not None:
506                if promotedTo in self.extensions:
507                    #print(f'{promotedTo} is promoted from {extname}')
508                    self.extensions[promotedTo].promotedFrom.add(extname)
509                    #print(f'setting self.extensions[{promotedTo}].promotedFrom = {self.extensions[promotedTo].promotedFrom}')
510                elif not self.conventions.is_api_version_name(promotedTo):
511                    self.logMsg('warn', f'{extname} is promoted to {promotedTo} which is not in the extension map')
512
513        # Generate metadoc extension files, in refpage and non-refpage form
514        for ext in self.extensions.values():
515            ext.makeMetafile(self.extensions, isRefpage = False)
516            if self.conventions.write_refpage_include:
517                ext.makeMetafile(self.extensions, isRefpage = True)
518
519        # Key to sort extensions alphabetically within 'KHR', 'EXT', vendor
520        # extension prefixes.
521        def makeSortKey(extname):
522            name = extname.lower()
523            prefixes = self.conventions.extension_index_prefixes
524            for i, prefix in enumerate(prefixes):
525                if extname.startswith(prefix):
526                    return (i, name)
527            return (len(prefixes), name)
528
529        # Generate list of promoted extensions
530        promotedExtensions = {}
531        for ext in self.extensions.values():
532            if ext.deprecationType == 'promotion' and ext.supercedingAPIVersion:
533                promotedExtensions.setdefault(ext.supercedingAPIVersion, []).append(ext.name)
534
535        for coreVersion, extensions in promotedExtensions.items():
536            promoted_extensions_fp = self.newFile(self.directory + '/promoted_extensions_' + coreVersion + self.file_suffix)
537
538            for extname in sorted(extensions, key=makeSortKey):
539                indent = ''
540                write('  * {blank}\n+\n' + ext.conditionalLinkExt(extname, indent), file=promoted_extensions_fp)
541
542            promoted_extensions_fp.close()
543
544        # Generate include directives for the extensions appendix, grouping
545        # extensions by status (current, deprecated, provisional, etc.)
546        with self.newFile(self.directory + '/current_extensions_appendix' + self.file_suffix) as current_extensions_appendix_fp, \
547                self.newFile(self.directory + '/deprecated_extensions_appendix' + self.file_suffix) as deprecated_extensions_appendix_fp, \
548                self.newFile(self.directory + '/current_extension_appendices' + self.file_suffix) as current_extension_appendices_fp, \
549                self.newFile(self.directory + '/current_extension_appendices_toc' + self.file_suffix) as current_extension_appendices_toc_fp, \
550                self.newFile(self.directory + '/deprecated_extension_appendices' + self.file_suffix) as deprecated_extension_appendices_fp, \
551                self.newFile(self.directory + '/deprecated_extension_appendices_toc' + self.file_suffix) as deprecated_extension_appendices_toc_fp, \
552                self.newFile(self.directory + '/deprecated_extensions_guard_macro' + self.file_suffix) as deprecated_extensions_guard_macro_fp, \
553                self.newFile(self.directory + '/provisional_extensions_appendix' + self.file_suffix) as provisional_extensions_appendix_fp, \
554                self.newFile(self.directory + '/provisional_extension_appendices' + self.file_suffix) as provisional_extension_appendices_fp, \
555                self.newFile(self.directory + '/provisional_extension_appendices_toc' + self.file_suffix) as provisional_extension_appendices_toc_fp, \
556                self.newFile(self.directory + '/provisional_extensions_guard_macro' + self.file_suffix) as provisional_extensions_guard_macro_fp:
557
558            # Note: there is a hardwired assumption in creating the
559            # include:: directives below that all of these files are located
560            # in the 'meta/' subdirectory of the generated files directory.
561            # This is difficult to change, and it is very unlikely changing
562            # it will be needed.
563
564            write('', file=current_extensions_appendix_fp)
565            write('include::{generated}/meta/deprecated_extensions_guard_macro' + self.file_suffix + '[]', file=current_extensions_appendix_fp)
566            write('', file=current_extensions_appendix_fp)
567            write('ifndef::HAS_DEPRECATED_EXTENSIONS[]', file=current_extensions_appendix_fp)
568            write('[[extension-appendices-list]]', file=current_extensions_appendix_fp)
569            write('== List of Extensions', file=current_extensions_appendix_fp)
570            write('endif::HAS_DEPRECATED_EXTENSIONS[]', file=current_extensions_appendix_fp)
571            write('ifdef::HAS_DEPRECATED_EXTENSIONS[]', file=current_extensions_appendix_fp)
572            write('[[extension-appendices-list]]', file=current_extensions_appendix_fp)
573            write('== List of Current Extensions', file=current_extensions_appendix_fp)
574            write('endif::HAS_DEPRECATED_EXTENSIONS[]', file=current_extensions_appendix_fp)
575            write('', file=current_extensions_appendix_fp)
576            write('include::{generated}/meta/current_extension_appendices_toc' + self.file_suffix + '[]', file=current_extensions_appendix_fp)
577            write('\n<<<\n', file=current_extensions_appendix_fp)
578            write('include::{generated}/meta/current_extension_appendices' + self.file_suffix + '[]', file=current_extensions_appendix_fp)
579
580            write('', file=deprecated_extensions_appendix_fp)
581            write('include::{generated}/meta/deprecated_extensions_guard_macro' + self.file_suffix + '[]', file=deprecated_extensions_appendix_fp)
582            write('', file=deprecated_extensions_appendix_fp)
583            write('ifdef::HAS_DEPRECATED_EXTENSIONS[]', file=deprecated_extensions_appendix_fp)
584            write('[[deprecated-extension-appendices-list]]', file=deprecated_extensions_appendix_fp)
585            write('== List of Deprecated Extensions', file=deprecated_extensions_appendix_fp)
586            write('include::{generated}/meta/deprecated_extension_appendices_toc' + self.file_suffix + '[]', file=deprecated_extensions_appendix_fp)
587            write('\n<<<\n', file=deprecated_extensions_appendix_fp)
588            write('include::{generated}/meta/deprecated_extension_appendices' + self.file_suffix + '[]', file=deprecated_extensions_appendix_fp)
589            write('endif::HAS_DEPRECATED_EXTENSIONS[]', file=deprecated_extensions_appendix_fp)
590
591            # add include guards to allow multiple includes
592            write('ifndef::DEPRECATED_EXTENSIONS_GUARD_MACRO_INCLUDE_GUARD[]', file=deprecated_extensions_guard_macro_fp)
593            write(':DEPRECATED_EXTENSIONS_GUARD_MACRO_INCLUDE_GUARD:\n', file=deprecated_extensions_guard_macro_fp)
594            write('ifndef::PROVISIONAL_EXTENSIONS_GUARD_MACRO_INCLUDE_GUARD[]', file=provisional_extensions_guard_macro_fp)
595            write(':PROVISIONAL_EXTENSIONS_GUARD_MACRO_INCLUDE_GUARD:\n', file=provisional_extensions_guard_macro_fp)
596
597            write('', file=provisional_extensions_appendix_fp)
598            write('include::{generated}/meta/provisional_extensions_guard_macro' + self.file_suffix + '[]', file=provisional_extensions_appendix_fp)
599            write('', file=provisional_extensions_appendix_fp)
600            write('ifdef::HAS_PROVISIONAL_EXTENSIONS[]', file=provisional_extensions_appendix_fp)
601            write('[[provisional-extension-appendices-list]]', file=provisional_extensions_appendix_fp)
602            write('== List of Provisional Extensions', file=provisional_extensions_appendix_fp)
603            write('include::{generated}/meta/provisional_extension_appendices_toc' + self.file_suffix + '[]', file=provisional_extensions_appendix_fp)
604            write('\n<<<\n', file=provisional_extensions_appendix_fp)
605            write('include::{generated}/meta/provisional_extension_appendices' + self.file_suffix + '[]', file=provisional_extensions_appendix_fp)
606            write('endif::HAS_PROVISIONAL_EXTENSIONS[]', file=provisional_extensions_appendix_fp)
607
608            # Emit extensions in author ID order
609            sorted_keys = sorted(self.extensions.keys(), key=makeSortKey)
610            for name in sorted_keys:
611                ext = self.extensions[name]
612
613                include = self.makeExtensionInclude(ext.name)
614                link = '  * ' + self.conventions.formatExtension(ext.name)
615                if ext.provisional == 'true':
616                    write(self.conditionalExt(ext.name, include), file=provisional_extension_appendices_fp)
617                    write(self.conditionalExt(ext.name, link), file=provisional_extension_appendices_toc_fp)
618                    write(self.conditionalExt(ext.name, ':HAS_PROVISIONAL_EXTENSIONS:'), file=provisional_extensions_guard_macro_fp)
619                elif ext.deprecationType is None:
620                    write(self.conditionalExt(ext.name, include), file=current_extension_appendices_fp)
621                    write(self.conditionalExt(ext.name, link), file=current_extension_appendices_toc_fp)
622                else:
623                    condition = ext.supercedingAPIVersion if ext.supercedingAPIVersion else ext.supercedingExtension  # potentially None too
624
625                    write(self.conditionalExt(ext.name, include, 'ifndef', condition), file=current_extension_appendices_fp)
626                    write(self.conditionalExt(ext.name, link, 'ifndef', condition), file=current_extension_appendices_toc_fp)
627
628                    write(self.conditionalExt(ext.name, include, 'ifdef', condition), file=deprecated_extension_appendices_fp)
629                    write(self.conditionalExt(ext.name, link, 'ifdef', condition), file=deprecated_extension_appendices_toc_fp)
630
631                    write(self.conditionalExt(ext.name, ':HAS_DEPRECATED_EXTENSIONS:', 'ifdef', condition), file=deprecated_extensions_guard_macro_fp)
632
633            write('endif::DEPRECATED_EXTENSIONS_GUARD_MACRO_INCLUDE_GUARD[]', file=deprecated_extensions_guard_macro_fp)
634            write('endif::PROVISIONAL_EXTENSIONS_GUARD_MACRO_INCLUDE_GUARD[]', file=provisional_extensions_guard_macro_fp)
635
636        OutputGenerator.endFile(self)
637
638    def beginFeature(self, interface, emit):
639        # Start processing in superclass
640        OutputGenerator.beginFeature(self, interface, emit)
641
642        if interface.tag != 'extension':
643            self.logMsg('diag', 'beginFeature: ignoring non-extension feature', self.featureName)
644            return
645
646        # These attributes must exist
647        name = self.featureName
648        number = self.getAttrib(interface, 'number')
649        ext_type = self.getAttrib(interface, 'type')
650        revision = self.getSpecVersion(interface, name)
651
652        # These attributes are optional
653        OPTIONAL = False
654        depends = self.getAttrib(interface, 'depends', OPTIONAL)    # TODO should default to VK_VERSION_1_0?
655        contact = self.getAttrib(interface, 'contact', OPTIONAL)
656        promotedTo = self.getAttrib(interface, 'promotedto', OPTIONAL)
657        deprecatedBy = self.getAttrib(interface, 'deprecatedby', OPTIONAL)
658        obsoletedBy = self.getAttrib(interface, 'obsoletedby', OPTIONAL)
659        provisional = self.getAttrib(interface, 'provisional', OPTIONAL, 'false')
660        specialuse = self.getAttrib(interface, 'specialuse', OPTIONAL)
661        ratified = self.getAttrib(interface, 'ratified', OPTIONAL, '')
662
663        filename = self.directory + '/' + name + self.file_suffix
664
665        extdata = Extension(
666            generator = self,
667            filename = filename,
668            name = name,
669            number = number,
670            ext_type = ext_type,
671            depends = depends,
672            contact = contact,
673            promotedTo = promotedTo,
674            deprecatedBy = deprecatedBy,
675            obsoletedBy = obsoletedBy,
676            provisional = provisional,
677            revision = revision,
678            specialuse = specialuse,
679            ratified = ratified)
680        self.extensions[name] = extdata
681
682    def endFeature(self):
683        # Finish processing in superclass
684        OutputGenerator.endFeature(self)
685
686    def getAttrib(self, elem, attribute, required=True, default=None):
687        """Query an attribute from an element, or return a default value
688
689        - elem - element to query
690        - attribute - attribute name
691        - required - whether attribute must exist
692        - default - default value if attribute not present"""
693        attrib = elem.get(attribute, default)
694        if required and (attrib is None):
695            name = elem.get('name', 'UNKNOWN')
696            self.logMsg('error', 'While processing \'' + self.featureName + ', <' + elem.tag + '> \'' + name + '\' does not contain required attribute \'' + attribute + '\'')
697        return attrib
698
699    def numbersToWords(self, name):
700        allowlist = ['WIN32', 'INT16', 'D3D1']
701
702        # temporarily replace allowlist items
703        for i, w in enumerate(allowlist):
704            name = re.sub(w, '{' + str(i) + '}', name)
705
706        name = re.sub(r'(?<=[A-Z])(\d+)(?![A-Z])', r'_\g<1>', name)
707
708        # undo allowlist substitution
709        for i, w in enumerate(allowlist):
710            name = re.sub('\\{' + str(i) + '}', w, name)
711
712        return name
713
714    def getSpecVersion(self, elem, extname, default=None):
715        """Determine the extension revision from the EXTENSION_NAME_SPEC_VERSION
716        enumerant.
717
718        - elem - <extension> element to query
719        - extname - extension name from the <extension> 'name' attribute
720        - default - default value if SPEC_VERSION token not present"""
721        # The literal enumerant name to match
722        versioningEnumName = self.numbersToWords(extname.upper()) + '_SPEC_VERSION'
723
724        for enum in elem.findall('./require/enum'):
725            enumName = self.getAttrib(enum, 'name')
726            if enumName == versioningEnumName:
727                return self.getAttrib(enum, 'value')
728
729        #if not found:
730        for enum in elem.findall('./require/enum'):
731            enumName = self.getAttrib(enum, 'name')
732            if enumName.find('SPEC_VERSION') != -1:
733                self.logMsg('diag', 'Missing ' + versioningEnumName + '! Potential misnamed candidate ' + enumName + '.')
734                return self.getAttrib(enum, 'value')
735
736        self.logMsg('error', 'Missing ' + versioningEnumName + '!')
737        return default
738