summaryrefslogtreecommitdiff
path: root/registry/vulkan/scripts/extensionmetadocgenerator.py
blob: 07d2c947c9b93bee3ee8944db2fc5d88d4bfa6dd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
#!/usr/bin/python3 -i
#
# Copyright 2013-2021 The Khronos Group Inc.
#
# SPDX-License-Identifier: Apache-2.0

import os
import re
import sys
from functools import total_ordering
from generator import GeneratorOptions, OutputGenerator, regSortFeatures, write

class ExtensionMetaDocGeneratorOptions(GeneratorOptions):
    """ExtensionMetaDocGeneratorOptions - subclass of GeneratorOptions.

    Represents options during extension metainformation generation for Asciidoc"""
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)


EXT_NAME_DECOMPOSE_RE = re.compile(r'[A-Z]+_(?P<tag>[A-Z]+)_(?P<name>[\w_]+)')


@total_ordering
class Extension:
    def __init__(self,
                 generator, # needed for logging and API conventions
                 filename,
                 name,
                 number,
                 ext_type,
                 requires,
                 requiresCore,
                 contact,
                 promotedTo,
                 deprecatedBy,
                 obsoletedBy,
                 provisional,
                 revision,
                 specialuse ):
        self.generator = generator
        self.conventions = generator.genOpts.conventions
        self.filename = filename
        self.name = name
        self.number = number
        self.ext_type = ext_type
        self.requires = requires
        self.requiresCore = requiresCore
        self.contact = contact
        self.promotedTo = promotedTo
        self.deprecatedBy = deprecatedBy
        self.obsoletedBy = obsoletedBy
        self.provisional = provisional
        self.revision = revision
        self.specialuse = specialuse

        self.deprecationType = None
        self.supercedingAPIVersion = None
        self.supercedingExtension = None

        if self.promotedTo is not None and self.deprecatedBy is not None and self.obsoletedBy is not None:
            self.generator.logMsg('warn', 'All \'promotedto\', \'deprecatedby\' and \'obsoletedby\' attributes used on extension ' + self.name + '! Ignoring \'promotedto\' and \'deprecatedby\'.')
        elif self.promotedTo is not None and self.deprecatedBy is not None:
            self.generator.logMsg('warn', 'Both \'promotedto\' and \'deprecatedby\' attributes used on extension ' + self.name + '! Ignoring \'deprecatedby\'.')
        elif self.promotedTo is not None and self.obsoletedBy is not None:
            self.generator.logMsg('warn', 'Both \'promotedto\' and \'obsoletedby\' attributes used on extension ' + self.name + '! Ignoring \'promotedto\'.')
        elif self.deprecatedBy is not None and self.obsoletedBy is not None:
            self.generator.logMsg('warn', 'Both \'deprecatedby\' and \'obsoletedby\' attributes used on extension ' + self.name + '! Ignoring \'deprecatedby\'.')

        supercededBy = None
        if self.promotedTo is not None:
            self.deprecationType = 'promotion'
            supercededBy = promotedTo
        elif self.deprecatedBy is not None:
            self.deprecationType = 'deprecation'
            supercededBy = deprecatedBy
        elif self.obsoletedBy is not None:
            self.deprecationType = 'obsoletion'
            supercededBy = obsoletedBy

        if supercededBy is not None:
            if supercededBy == '' and not self.deprecationType == 'promotion':
                pass # supercedingAPIVersion, supercedingExtension is None
            elif supercededBy.startswith(self.conventions.api_version_prefix):
                self.supercedingAPIVersion = supercededBy
            elif supercededBy.startswith(self.conventions.api_prefix):
                self.supercedingExtension = supercededBy
            else:
                self.generator.logMsg('error', 'Unrecognized ' + self.deprecationType + ' attribute value \'' + supercededBy + '\'!')

        match = EXT_NAME_DECOMPOSE_RE.match(self.name)
        self.vendor = match.group('tag')
        self.bare_name = match.group('name')

    def __str__(self):
        return self.name
    def __eq__(self, other):
        return self.name == other.name
    def __ne__(self, other):
        return self.name != other.name

    def __lt__(self, other):
        self_is_KHR = self.name.startswith(self.conventions.KHR_prefix)
        self_is_EXT = self.name.startswith(self.conventions.EXT_prefix)
        other_is_KHR = other.name.startswith(self.conventions.KHR_prefix)
        other_is_EXT = other.name.startswith(self.conventions.EXT_prefix)

        swap = False
        if self_is_KHR and not other_is_KHR:
            return not swap
        if other_is_KHR and not self_is_KHR:
            return swap
        if self_is_EXT and not other_is_EXT:
            return not swap
        if other_is_EXT and not self_is_EXT:
            return swap

        return self.name < other.name

    def typeToStr(self):
        if self.ext_type == 'instance':
            return 'Instance extension'
        if self.ext_type == 'device':
            return 'Device extension'

        if self.ext_type is not None:
            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).')
        else: # should be unreachable
            self.generator.logMsg('error', 'Logic error in typeToStr(): Missing type attribute!')
        return None

    def specLink(self, xrefName, xrefText, isRefpage = False):
        """Generate a string containing a link to a specification anchor in
           asciidoctor markup form.

        - xrefName - anchor name in the spec
        - xrefText - text to show for the link, or None
        - isRefpage = True if generating a refpage include, False if
          generating a specification extension appendix include"""

        if isRefpage:
            # Always link into API spec
            specURL = self.conventions.specURL('api')
            return 'link:{}#{}[{}^]'.format(specURL, xrefName, xrefText)
        else:
            return '<<' + xrefName + ', ' + xrefText + '>>'

    def conditionalLinkCoreAPI(self, apiVersion, linkSuffix, isRefpage):
        versionMatch = re.match(self.conventions.api_version_prefix + r'(\d+)_(\d+)', apiVersion)
        major = versionMatch.group(1)
        minor = versionMatch.group(2)

        dottedVersion = major + '.' + minor

        xrefName = 'versions-' + dottedVersion + linkSuffix
        xrefText = self.conventions.api_name() + ' ' + dottedVersion

        doc  = 'ifdef::' + apiVersion + '[]\n'
        doc += '    ' + self.specLink(xrefName, xrefText, isRefpage) + '\n'
        doc += 'endif::' + apiVersion + '[]\n'
        doc += 'ifndef::' + apiVersion + '[]\n'
        doc += '    ' + self.conventions.api_name() + ' ' + dottedVersion + '\n'
        doc += 'endif::' + apiVersion + '[]\n'

        return doc

    def conditionalLinkExt(self, extName, indent = '    '):
        doc  = 'ifdef::' + extName + '[]\n'
        doc +=  indent + self.conventions.formatExtension(extName) + '\n'
        doc += 'endif::' + extName + '[]\n'
        doc += 'ifndef::' + extName + '[]\n'
        doc += indent + '`' + extName + '`\n'
        doc += 'endif::' + extName + '[]\n'

        return doc

    def resolveDeprecationChain(self, extensionsList, succeededBy, isRefpage, file):
        ext = next(x for x in extensionsList if x.name == succeededBy)

        if ext.deprecationType:
            if ext.deprecationType == 'promotion':
                if ext.supercedingAPIVersion:
                    write('  ** Which in turn was _promoted_ to\n' + ext.conditionalLinkCoreAPI(ext.supercedingAPIVersion, '-promotions', isRefpage), file=file)
                else: # ext.supercedingExtension
                    write('  ** Which in turn was _promoted_ to extension\n' + ext.conditionalLinkExt(ext.supercedingExtension), file=file)
                    ext.resolveDeprecationChain(extensionsList, ext.supercedingExtension, file)
            elif ext.deprecationType == 'deprecation':
                if ext.supercedingAPIVersion:
                    write('  ** Which in turn was _deprecated_ by\n' + ext.conditionalLinkCoreAPI(ext.supercedingAPIVersion, '-new-feature', isRefpage), file=file)
                elif ext.supercedingExtension:
                    write('  ** Which in turn was _deprecated_ by\n' + ext.conditionalLinkExt(ext.supercedingExtension) + '    extension', file=file)
                    ext.resolveDeprecationChain(extensionsList, ext.supercedingExtension, file)
                else:
                    write('  ** Which in turn was _deprecated_ without replacement', file=file)
            elif ext.deprecationType == 'obsoletion':
                if ext.supercedingAPIVersion:
                    write('  ** Which in turn was _obsoleted_ by\n' + ext.conditionalLinkCoreAPI(ext.supercedingAPIVersion, '-new-feature', isRefpage), file=file)
                elif ext.supercedingExtension:
                    write('  ** Which in turn was _obsoleted_ by\n' + ext.conditionalLinkExt(ext.supercedingExtension) + '    extension', file=file)
                    ext.resolveDeprecationChain(extensionsList, ext.supercedingExtension, file)
                else:
                    write('  ** Which in turn was _obsoleted_ without replacement', file=file)
            else: # should be unreachable
                self.generator.logMsg('error', 'Logic error in resolveDeprecationChain(): deprecationType is neither \'promotion\', \'deprecation\' nor \'obsoletion\'!')


    def writeTag(self, tag, value, isRefpage, fp):
        """Write a tag and (if non-None) a tag value to a file.

        - tag - string tag name
        - value - tag value, or None
        - isRefpage - controls style in which the tag is marked up
        - fp - open file pointer to write to"""

        if isRefpage:
            # Use subsection headers for the tag name
            tagPrefix = '== '
            tagSuffix = ''
        else:
            # Use an bolded item list for the tag name
            tagPrefix = '*'
            tagSuffix = '*::'

        write(tagPrefix + tag + tagSuffix, file=fp)
        if value is not None:
            write(value, file=fp)

        if isRefpage:
            write('', file=fp)

    def makeMetafile(self, extensionsList, isRefpage = False):
        """Generate a file containing extension metainformation in
           asciidoctor markup form.

        - extensionsList - list of extensions spec is being generated against
        - isRefpage - True if generating a refpage include, False if
          generating a specification extension appendix include"""

        if isRefpage:
            filename = self.filename.replace('meta/', 'meta/refpage.')
        else:
            filename = self.filename

        fp = self.generator.newFile(filename)

        if not isRefpage:
            write('[[' + self.name + ']]', file=fp)
            write('=== ' + self.name, file=fp)
            write('', file=fp)

            self.writeTag('Name String', '`' + self.name + '`', isRefpage, fp)
            self.writeTag('Extension Type', self.typeToStr(), isRefpage, fp)

        self.writeTag('Registered Extension Number', self.number, isRefpage, fp)
        self.writeTag('Revision', self.revision, isRefpage, fp)

        # Only API extension dependencies are coded in XML, others are explicit
        self.writeTag('Extension and Version Dependencies', None, isRefpage, fp)

        write('  * Requires ' + self.conventions.api_name() + ' ' + self.requiresCore, file=fp)
        if self.requires:
            for dep in self.requires.split(','):
                write('  * Requires', self.conventions.formatExtension(dep),
                      file=fp)
        if self.provisional == 'true':
            write('  * *This is a _provisional_ extension and must: be used with caution.', file=fp)
            write('    See the ' +
                  self.specLink(xrefName = 'boilerplate-provisional-header',
                                xrefText = 'description',
                                isRefpage = isRefpage) +
                  ' of provisional header files for enablement and stability details.*', file=fp)
        write('', file=fp)

        if self.deprecationType:
            self.writeTag('Deprecation state', None, isRefpage, fp)

            if self.deprecationType == 'promotion':
                if self.supercedingAPIVersion:
                    write('  * _Promoted_ to\n' + self.conditionalLinkCoreAPI(self.supercedingAPIVersion, '-promotions', isRefpage), file=fp)
                else: # ext.supercedingExtension
                    write('  * _Promoted_ to\n' + self.conditionalLinkExt(self.supercedingExtension) + '    extension', file=fp)
                    self.resolveDeprecationChain(extensionsList, self.supercedingExtension, isRefpage, fp)
            elif self.deprecationType == 'deprecation':
                if self.supercedingAPIVersion:
                    write('  * _Deprecated_ by\n' + self.conditionalLinkCoreAPI(self.supercedingAPIVersion, '-new-features', isRefpage), file=fp)
                elif self.supercedingExtension:
                    write('  * _Deprecated_ by\n' + self.conditionalLinkExt(self.supercedingExtension) + '    extension' , file=fp)
                    self.resolveDeprecationChain(extensionsList, self.supercedingExtension, isRefpage, fp)
                else:
                    write('  * _Deprecated_ without replacement' , file=fp)
            elif self.deprecationType == 'obsoletion':
                if self.supercedingAPIVersion:
                    write('  * _Obsoleted_ by\n' + self.conditionalLinkCoreAPI(self.supercedingAPIVersion, '-new-features', isRefpage), file=fp)
                elif self.supercedingExtension:
                    write('  * _Obsoleted_ by\n' + self.conditionalLinkExt(self.supercedingExtension) + '    extension' , file=fp)
                    self.resolveDeprecationChain(extensionsList, self.supercedingExtension, isRefpage, fp)
                else:
                    # TODO: Does not make sense to retroactively ban use of extensions from 1.0.
                    #       Needs some tweaks to the semantics and this message, when such extension(s) occur.
                    write('  * _Obsoleted_ without replacement' , file=fp)
            else: # should be unreachable
                self.generator.logMsg('error', 'Logic error in makeMetafile(): deprecationType is neither \'promotion\', \'deprecation\' nor \'obsoletion\'!')
            write('', file=fp)

        if self.specialuse is not None:
            specialuses = self.specialuse.split(',')
            if len(specialuses) > 1:
                header = 'Special Uses'
            else:
                header = 'Special Use'
            self.writeTag(header, None, isRefpage, fp)

            for use in specialuses:
                # Each specialuse attribute value expands an asciidoctor
                # attribute of the same name, instead of using the shorter,
                # and harder to understand attribute
                write('* {}'.format(
                      self.specLink(
                           xrefName = self.conventions.special_use_section_anchor,
                           xrefText = '{' + use + '}',
                           isRefpage = isRefpage)), file=fp)
            write('', file=fp)

        if self.conventions.write_contacts:
            self.writeTag('Contact', None, isRefpage, fp)

            contacts = self.contact.split(',')
            for contact in contacts:
                contactWords = contact.strip().split()
                name = ' '.join(contactWords[:-1])
                handle = contactWords[-1]
                if handle.startswith('gitlab:'):
                    prettyHandle = 'icon:gitlab[alt=GitLab, role="red"]' + handle.replace('gitlab:@', '')
                elif handle.startswith('@'):
                    issuePlaceholderText = '[' + self.name + '] ' + handle
                    issuePlaceholderText += '%0A<<Here describe the issue or question you have about the ' + self.name + ' extension>>'
                    trackerLink = 'link:++https://github.com/KhronosGroup/Vulkan-Docs/issues/new?body=' + issuePlaceholderText + '++'
                    prettyHandle = trackerLink + '[icon:github[alt=GitHub,role="black"]' + handle[1:] + ',window=_blank,opts=nofollow]'
                else:
                    prettyHandle = handle

                write('  * ' + name + ' ' + prettyHandle, file=fp)
            write('', file=fp)

        # Check if a proposal document for this extension exists in the
        # current repository, and link to the same document (parameterized
        # by a URL prefix attribute) if it does.
        # The assumption is that a proposal document for an extension
        # VK_name will be located in 'proposals/VK_name.asciidoc' relative
        # to the repository root, and that this script will be invoked from
        # the repository root.
        path = 'proposals/{}.asciidoc'.format(self.name)
        if os.path.exists(path) and os.access(path, os.R_OK):
            self.writeTag('Extension Proposal',
                'link:{{specRepositoryURL}}/{}[{}]'.format(path, self.name), isRefpage, fp)

        # If this is metadata to be included in a refpage, adjust the
        # leveloffset to account for the relative structure of the extension
        # appendices vs. refpages.
        if isRefpage:
            write(':leveloffset: -1', file=fp)

        fp.close()

class ExtensionMetaDocOutputGenerator(OutputGenerator):
    """ExtensionMetaDocOutputGenerator - subclass of OutputGenerator.

    Generates AsciiDoc includes with metainformation for the API extension
    appendices. The fields used from <extension> tags in the API XML are:

    - name          extension name string
    - number        extension number (optional)
    - contact       name and GitHub login or email address (optional)
    - type          'instance' | 'device' (optional)
    - requires      list of comma-separated required API extensions (optional)
    - requiresCore  required core version of API (optional)
    - promotedTo    extension or API version it was promoted to
    - deprecatedBy  extension or API version which deprecated this extension,
                    or empty string if deprecated without replacement
    - obsoletedBy   extension or API version which obsoleted this extension,
                    or empty string if obsoleted without replacement
    - provisional   'true' if this extension is released provisionally"""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.extensions = []
        # List of strings containing all vendor tags
        self.vendor_tags = []
        self.file_suffix = ''

    def newFile(self, filename):
        self.logMsg('diag', '# Generating include file:', filename)
        fp = open(filename, 'w', encoding='utf-8')
        write(self.genOpts.conventions.warning_comment, file=fp)
        return fp

    def beginFile(self, genOpts):
        OutputGenerator.beginFile(self, genOpts)

        self.directory = self.genOpts.directory
        self.file_suffix = self.genOpts.conventions.file_suffix

        # Iterate over all 'tag' Elements and add the names of all the valid vendor
        # tags to the list
        root = self.registry.tree.getroot()
        for tag in root.findall('tags/tag'):
            self.vendor_tags.append(tag.get('name'))

        # Create subdirectory, if needed
        self.makeDir(self.directory)

    def conditionalExt(self, extName, content, ifdef = None, condition = None):
        doc = ''

        innerdoc  = 'ifdef::' + extName + '[]\n'
        innerdoc += content + '\n'
        innerdoc += 'endif::' + extName + '[]\n'

        if ifdef:
            if ifdef == 'ifndef':
                if condition:
                    doc += 'ifndef::' + condition + '[]\n'
                    doc += innerdoc
                    doc += 'endif::' + condition + '[]\n'
                else: # no condition is as if condition is defined; "nothing" is always defined :p
                    pass # so no output
            elif ifdef == 'ifdef':
                if condition:
                    doc += 'ifdef::' + condition + '+' + extName + '[]\n'
                    doc += content + '\n' # does not include innerdoc; the ifdef was merged with the one above
                    doc += 'endif::' + condition + '+' + extName + '[]\n'
                else: # no condition is as if condition is defined; "nothing" is always defined :p
                    doc += innerdoc
            else: # should be unreachable
                raise RuntimeError('Should be unreachable: ifdef is neither \'ifdef \' nor \'ifndef\'!')
        else:
            doc += innerdoc

        return doc

    def makeExtensionInclude(self, ext):
        return self.conventions.extension_include_string(ext)

    def endFile(self):
        self.extensions.sort()

        # Generate metadoc extension files, in refpage and non-refpage form
        for ext in self.extensions:
            ext.makeMetafile(self.extensions, isRefpage = False)
            if self.conventions.write_refpage_include:
                ext.makeMetafile(self.extensions, isRefpage = True)

        # Generate list of promoted extensions
        promotedExtensions = {}
        for ext in self.extensions:
            if ext.deprecationType == 'promotion' and ext.supercedingAPIVersion:
                promotedExtensions.setdefault(ext.supercedingAPIVersion, []).append(ext)

        for coreVersion, extensions in promotedExtensions.items():
            promoted_extensions_fp = self.newFile(self.directory + '/promoted_extensions_' + coreVersion + self.file_suffix)

            for ext in extensions:
                indent = ''
                write('  * {blank}\n+\n' + ext.conditionalLinkExt(ext.name, indent), file=promoted_extensions_fp)

            promoted_extensions_fp.close()

        # Re-sort to match earlier behavior
        # TODO: Remove this extra sort when re-arranging section order OK.

        def makeSortKey(ext):
            name = ext.name.lower()
            prefixes = self.conventions.extension_index_prefixes
            for i, prefix in enumerate(prefixes):
                if ext.name.startswith(prefix):
                    return (i, name)
            return (len(prefixes), name)

        self.extensions.sort(key=makeSortKey)

        # Generate include directives for the extensions appendix, grouping
        # extensions by status (current, deprecated, provisional, etc.)
        with self.newFile(self.directory + '/current_extensions_appendix' + self.file_suffix) as current_extensions_appendix_fp, \
                self.newFile(self.directory + '/deprecated_extensions_appendix' + self.file_suffix) as deprecated_extensions_appendix_fp, \
                self.newFile(self.directory + '/current_extension_appendices' + self.file_suffix) as current_extension_appendices_fp, \
                self.newFile(self.directory + '/current_extension_appendices_toc' + self.file_suffix) as current_extension_appendices_toc_fp, \
                self.newFile(self.directory + '/deprecated_extension_appendices' + self.file_suffix) as deprecated_extension_appendices_fp, \
                self.newFile(self.directory + '/deprecated_extension_appendices_toc' + self.file_suffix) as deprecated_extension_appendices_toc_fp, \
                self.newFile(self.directory + '/deprecated_extensions_guard_macro' + self.file_suffix) as deprecated_extensions_guard_macro_fp, \
                self.newFile(self.directory + '/provisional_extensions_appendix' + self.file_suffix) as provisional_extensions_appendix_fp, \
                self.newFile(self.directory + '/provisional_extension_appendices' + self.file_suffix) as provisional_extension_appendices_fp, \
                self.newFile(self.directory + '/provisional_extension_appendices_toc' + self.file_suffix) as provisional_extension_appendices_toc_fp, \
                self.newFile(self.directory + '/provisional_extensions_guard_macro' + self.file_suffix) as provisional_extensions_guard_macro_fp:

            write('', file=current_extensions_appendix_fp)
            write('include::deprecated_extensions_guard_macro' + self.file_suffix + '[]', file=current_extensions_appendix_fp)
            write('', file=current_extensions_appendix_fp)
            write('ifndef::HAS_DEPRECATED_EXTENSIONS[]', file=current_extensions_appendix_fp)
            write('[[extension-appendices-list]]', file=current_extensions_appendix_fp)
            write('== List of Extensions', file=current_extensions_appendix_fp)
            write('endif::HAS_DEPRECATED_EXTENSIONS[]', file=current_extensions_appendix_fp)
            write('ifdef::HAS_DEPRECATED_EXTENSIONS[]', file=current_extensions_appendix_fp)
            write('[[extension-appendices-list]]', file=current_extensions_appendix_fp)
            write('== List of Current Extensions', file=current_extensions_appendix_fp)
            write('endif::HAS_DEPRECATED_EXTENSIONS[]', file=current_extensions_appendix_fp)
            write('', file=current_extensions_appendix_fp)
            write('include::current_extension_appendices_toc' + self.file_suffix + '[]', file=current_extensions_appendix_fp)
            write('\n<<<\n', file=current_extensions_appendix_fp)
            write('include::current_extension_appendices' + self.file_suffix + '[]', file=current_extensions_appendix_fp)

            write('', file=deprecated_extensions_appendix_fp)
            write('include::deprecated_extensions_guard_macro' + self.file_suffix + '[]', file=deprecated_extensions_appendix_fp)
            write('', file=deprecated_extensions_appendix_fp)
            write('ifdef::HAS_DEPRECATED_EXTENSIONS[]', file=deprecated_extensions_appendix_fp)
            write('[[deprecated-extension-appendices-list]]', file=deprecated_extensions_appendix_fp)
            write('== List of Deprecated Extensions', file=deprecated_extensions_appendix_fp)
            write('include::deprecated_extension_appendices_toc' + self.file_suffix + '[]', file=deprecated_extensions_appendix_fp)
            write('\n<<<\n', file=deprecated_extensions_appendix_fp)
            write('include::deprecated_extension_appendices' + self.file_suffix + '[]', file=deprecated_extensions_appendix_fp)
            write('endif::HAS_DEPRECATED_EXTENSIONS[]', file=deprecated_extensions_appendix_fp)

            # add include guards to allow multiple includes
            write('ifndef::DEPRECATED_EXTENSIONS_GUARD_MACRO_INCLUDE_GUARD[]', file=deprecated_extensions_guard_macro_fp)
            write(':DEPRECATED_EXTENSIONS_GUARD_MACRO_INCLUDE_GUARD:\n', file=deprecated_extensions_guard_macro_fp)
            write('ifndef::PROVISIONAL_EXTENSIONS_GUARD_MACRO_INCLUDE_GUARD[]', file=provisional_extensions_guard_macro_fp)
            write(':PROVISIONAL_EXTENSIONS_GUARD_MACRO_INCLUDE_GUARD:\n', file=provisional_extensions_guard_macro_fp)

            write('', file=provisional_extensions_appendix_fp)
            write('include::provisional_extensions_guard_macro' + self.file_suffix + '[]', file=provisional_extensions_appendix_fp)
            write('', file=provisional_extensions_appendix_fp)
            write('ifdef::HAS_PROVISIONAL_EXTENSIONS[]', file=provisional_extensions_appendix_fp)
            write('[[provisional-extension-appendices-list]]', file=provisional_extensions_appendix_fp)
            write('== List of Provisional Extensions', file=provisional_extensions_appendix_fp)
            write('include::provisional_extension_appendices_toc' + self.file_suffix + '[]', file=provisional_extensions_appendix_fp)
            write('\n<<<\n', file=provisional_extensions_appendix_fp)
            write('include::provisional_extension_appendices' + self.file_suffix + '[]', file=provisional_extensions_appendix_fp)
            write('endif::HAS_PROVISIONAL_EXTENSIONS[]', file=provisional_extensions_appendix_fp)

            for ext in self.extensions:
                include = self.makeExtensionInclude(ext)
                link = '  * ' + self.conventions.formatExtension(ext.name)
                if ext.provisional == 'true':
                    write(self.conditionalExt(ext.name, include), file=provisional_extension_appendices_fp)
                    write(self.conditionalExt(ext.name, link), file=provisional_extension_appendices_toc_fp)
                    write(self.conditionalExt(ext.name, ':HAS_PROVISIONAL_EXTENSIONS:'), file=provisional_extensions_guard_macro_fp)
                elif ext.deprecationType is None:
                    write(self.conditionalExt(ext.name, include), file=current_extension_appendices_fp)
                    write(self.conditionalExt(ext.name, link), file=current_extension_appendices_toc_fp)
                else:
                    condition = ext.supercedingAPIVersion if ext.supercedingAPIVersion else ext.supercedingExtension  # potentially None too

                    write(self.conditionalExt(ext.name, include, 'ifndef', condition), file=current_extension_appendices_fp)
                    write(self.conditionalExt(ext.name, link, 'ifndef', condition), file=current_extension_appendices_toc_fp)

                    write(self.conditionalExt(ext.name, include, 'ifdef', condition), file=deprecated_extension_appendices_fp)
                    write(self.conditionalExt(ext.name, link, 'ifdef', condition), file=deprecated_extension_appendices_toc_fp)

                    write(self.conditionalExt(ext.name, ':HAS_DEPRECATED_EXTENSIONS:', 'ifdef', condition), file=deprecated_extensions_guard_macro_fp)

            write('endif::DEPRECATED_EXTENSIONS_GUARD_MACRO_INCLUDE_GUARD[]', file=deprecated_extensions_guard_macro_fp)
            write('endif::PROVISIONAL_EXTENSIONS_GUARD_MACRO_INCLUDE_GUARD[]', file=provisional_extensions_guard_macro_fp)

        OutputGenerator.endFile(self)

    def beginFeature(self, interface, emit):
        # Start processing in superclass
        OutputGenerator.beginFeature(self, interface, emit)

        if interface.tag != 'extension':
            self.logMsg('diag', 'beginFeature: ignoring non-extension feature', self.featureName)
            return

        # These attributes must exist
        name = self.featureName
        number = self.getAttrib(interface, 'number')
        ext_type = self.getAttrib(interface, 'type')
        revision = self.getSpecVersion(interface, name)

        # These attributes are optional
        OPTIONAL = False
        requires = self.getAttrib(interface, 'requires', OPTIONAL)
        requiresCore = self.getAttrib(interface, 'requiresCore', OPTIONAL, '1.0') # TODO update this line with update_version.py
        contact = self.getAttrib(interface, 'contact', OPTIONAL)
        promotedTo = self.getAttrib(interface, 'promotedto', OPTIONAL)
        deprecatedBy = self.getAttrib(interface, 'deprecatedby', OPTIONAL)
        obsoletedBy = self.getAttrib(interface, 'obsoletedby', OPTIONAL)
        provisional = self.getAttrib(interface, 'provisional', OPTIONAL, 'false')
        specialuse = self.getAttrib(interface, 'specialuse', OPTIONAL)

        filename = self.directory + '/' + name + self.file_suffix

        extdata = Extension(
            generator = self,
            filename = filename,
            name = name,
            number = number,
            ext_type = ext_type,
            requires = requires,
            requiresCore = requiresCore,
            contact = contact,
            promotedTo = promotedTo,
            deprecatedBy = deprecatedBy,
            obsoletedBy = obsoletedBy,
            provisional = provisional,
            revision = revision,
            specialuse = specialuse)
        self.extensions.append(extdata)


    def endFeature(self):
        # Finish processing in superclass
        OutputGenerator.endFeature(self)

    def getAttrib(self, elem, attribute, required=True, default=None):
        """Query an attribute from an element, or return a default value

        - elem - element to query
        - attribute - attribute name
        - required - whether attribute must exist
        - default - default value if attribute not present"""
        attrib = elem.get(attribute, default)
        if required and (attrib is None):
            name = elem.get('name', 'UNKNOWN')
            self.logMsg('error', 'While processing \'' + self.featureName + ', <' + elem.tag + '> \'' + name + '\' does not contain required attribute \'' + attribute + '\'')
        return attrib

    def numbersToWords(self, name):
        allowlist = ['WIN32', 'INT16', 'D3D1']

        # temporarily replace allowlist items
        for i, w in enumerate(allowlist):
            name = re.sub(w, '{' + str(i) + '}', name)

        name = re.sub(r'(?<=[A-Z])(\d+)(?![A-Z])', r'_\g<1>', name)

        # undo allowlist substitution
        for i, w in enumerate(allowlist):
            name = re.sub('\\{' + str(i) + '}', w, name)

        return name

    def getSpecVersion(self, elem, extname, default=None):
        """Determine the extension revision from the EXTENSION_NAME_SPEC_VERSION
        enumerant.

        - elem - <extension> element to query
        - extname - extension name from the <extension> 'name' attribute
        - default - default value if SPEC_VERSION token not present"""
        # The literal enumerant name to match
        versioningEnumName = self.numbersToWords(extname.upper()) + '_SPEC_VERSION'

        for enum in elem.findall('./require/enum'):
            enumName = self.getAttrib(enum, 'name')
            if enumName == versioningEnumName:
                return self.getAttrib(enum, 'value')

        #if not found:
        for enum in elem.findall('./require/enum'):
            enumName = self.getAttrib(enum, 'name')
            if enumName.find('SPEC_VERSION') != -1:
                self.logMsg('diag', 'Missing ' + versioningEnumName + '! Potential misnamed candidate ' + enumName + '.')
                return self.getAttrib(enum, 'value')

        self.logMsg('error', 'Missing ' + versioningEnumName + '!')
        return default