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