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