• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2016-2021 The Khronos Group Inc.
2#
3# SPDX-License-Identifier: Apache-2.0
4
5require 'asciidoctor/extensions' unless RUBY_ENGINE == 'opal'
6
7include ::Asciidoctor
8
9module Asciidoctor
10
11class ValidUsageToJsonPreprocessorReader < PreprocessorReader
12  def process_line line
13    if line.start_with?( 'ifdef::VK_', 'ifndef::VK_', 'endif::VK_')
14      # Turn extension ifdefs into list items for when we're processing VU later.
15      return super('* ' + line)
16    else
17      return super(line)
18    end
19  end
20end
21
22# Preprocessor hook to iterate over ifdefs to prevent them from affecting asciidoctor's processing.
23class ValidUsageToJsonPreprocessor < Extensions::Preprocessor
24
25  def process document, reader
26    # Create a new reader to return, which handles turning the extension ifdefs into something else.
27    extension_preprocessor_reader = ValidUsageToJsonPreprocessorReader.new(document, reader.lines)
28
29    detected_vuid_list = []
30    extension_stack = []
31    in_validusage = :outside
32
33    # Despite replacing lines in the overridden preprocessor reader, a
34    # FIXME in Reader#peek_line suggests that this doesn't work, the new lines are simply discarded.
35    # So we just run over the new lines and do the replacement again.
36    new_lines = extension_preprocessor_reader.read_lines().flat_map do | line |
37
38      # Track whether we're in a VU block or not
39      if line.start_with?(".Valid Usage")
40        in_validusage = :about_to_enter  # About to enter VU
41      elsif in_validusage == :about_to_enter and line == '****'
42        in_validusage = :inside   # Entered VU block
43        extension_stack.each
44      elsif in_validusage == :inside and line == '****'
45        in_validusage = :outside   # Exited VU block
46      end
47
48      # Track extensions outside of the VU
49      if in_validusage == :outside and line.start_with?( 'ifdef::VK_', 'ifndef::VK_') and line.end_with?( '[]')
50        extension_stack.push line
51      elsif in_validusage == :outside and line.start_with?( 'endif::VK_')
52        extension_stack.pop
53      end
54
55      if in_validusage == :inside and line == '****'
56        # Write out the extension stack as bullets after this line
57        returned_lines = [line]
58        extension_stack.each do | extension |
59          returned_lines << ('* ' + extension)
60          # Add extra blank line to avoid this item absorbing any markup such as attributes on the next line
61          returned_lines << ''
62        end
63        returned_lines
64      elsif in_validusage == :inside and line.start_with?( 'ifdef::VK_', 'ifndef::VK_', 'endif::VK_') and line.end_with?('[]')
65        # Turn extension ifdefs into list items for when we're processing VU later.
66        ['* ' + line]
67      elsif in_validusage == :outside and line.start_with?( 'ifdef::VK_', 'ifndef::VK_', 'endif::VK_') and line.end_with?('[]')
68        # Remove the extension defines from the new lines, as we've dealt with them
69        []
70      elsif line.match(/\[\[(VUID-([^-]+)-[^\]]+)\]\]/)
71        # Add all the VUIDs into an array to guarantee they're all caught later.
72        detected_vuid_list << line.match(/(VUID-([^-]+)-[^\]]+)/)[0]
73        [line]
74      else
75        [line]
76      end
77    end
78
79    # Stash the detected vuids into a document attribute
80    document.set_attribute('detected_vuid_list', detected_vuid_list.join("\n"))
81
82    # Return a new reader after preprocessing
83    Reader.new(new_lines)
84  end
85end
86
87require 'json'
88class ValidUsageToJsonTreeprocessor < Extensions::Treeprocessor
89  def process document
90    map = {}
91
92    # Get the global vuid list
93    detected_vuid_list = document.attr('detected_vuid_list').split("\n")
94
95    map['version info'] = {
96      'schema version' => 2,
97      'api version' => document.attr('revnumber'),
98      'comment' => document.attr('revremark'),
99      'date' => document.attr('revdate')
100    }
101
102    map['validation'] = {}
103
104    error_found = false
105
106    # Need to find all valid usage blocks within a structure or function ref page section
107
108    # This is a list of all refpage types that may contain VUs
109    vu_refpage_types = [
110            'builtins',
111            'funcpointers',
112            'protos',
113            'spirv',
114            'structs',
115        ]
116
117    # Find all the open blocks
118    (document.find_by context: :open).each do |openblock|
119      # Filter out anything that's not a refpage
120      if openblock.attributes['refpage']
121        if vu_refpage_types.include? openblock.attributes['type']
122          parent = openblock.attributes['refpage']
123          # Find all the sidebars
124          (openblock.find_by context: :sidebar).each do |sidebar|
125            # Filter only the valid usage sidebars
126            if sidebar.title == "Valid Usage" || sidebar.title == "Valid Usage (Implicit)"
127              extensions = []
128              # There should be only one block - but just in case...
129              sidebar.blocks.each do |list|
130                # Iterate through all the items in the block, tracking which extensions are enabled/disabled.
131
132                attribute_replacements = list.attributes[:attribute_entries]
133
134                list.blocks.each do |item|
135                  if item.text.start_with?('ifdef::VK_')
136                    extensions << '(' + item.text[('ifdef::'.length)..-3] + ')'                # Look for "ifdef" directives and add them to the list of extensions
137                  elsif item.text.start_with?('ifndef::VK_')
138                    extensions << '!(' + item.text[('ifndef::'.length)..-3] + ')'              # Ditto for "ifndef" directives
139                  elsif item.text.start_with?('endif::VK_')
140                    extensions.slice!(-1)                                                      # Remove the last element when encountering an endif
141                  else
142                    item_text = item.text.clone
143
144                    # Replace the refpage if it's present
145                    item_text.gsub!(/\{refpage\}/i, parent)
146
147                    # Replace any attributes specified on the list (e.g. stageMask)
148                    if attribute_replacements
149                      attribute_replacements.each do |replacement|
150                        replacement_str = '\{' + replacement.name + '\}'
151                        replacement_regex = Regexp.new(replacement_str, Regexp::IGNORECASE)
152                        item_text.gsub!(replacement_regex, replacement.value)
153                      end
154                    end
155
156                    match = nil
157                    if item.text == item_text
158                      # The VUID will have been converted to a href in the general case, so find that
159                      match = /<a id=\"(VUID-[^"]+)\"[^>]*><\/a>(.*)/m.match(item_text)
160                    else
161                      # If we're doing manual attribute replacement, have to find the text of the anchor
162                      match = /\[\[(VUID-[^\]]+)\]\](.*)/m.match(item_text) # Otherwise, look for the VUID.
163                    end
164
165                    if (match != nil)
166                      vuid     = match[1]
167                      text     = match[2].gsub("\n", ' ')  # Have to forcibly remove newline characters; for some reason they're translated to the literally '\n' when converting to json.
168
169                      # Delete the vuid from the detected vuid list, so we know it's been extracted successfully
170                      if item.text == item_text
171                        # Simple if the item text hasn't been modified
172                        detected_vuid_list.delete(match[1])
173                      else
174                        # If the item text has been modified, get the vuid from the unmodified text
175                        detected_vuid_list.delete(/\[\[(VUID-([^-]+)-[^\]]+)\]\](.*)/m.match(item.text)[1])
176                      end
177
178                      # Generate the table entry
179                      entry = {'vuid' => vuid, 'text' => text}
180
181                      # Initialize the database if necessary
182                      if map['validation'][parent] == nil
183                        map['validation'][parent] = {}
184                      end
185
186                      # Figure out the name of the section the entry will be added in
187                      if extensions == []
188                        entry_section = 'core'
189                      else
190                        entry_section = extensions.join('+')
191                      end
192
193                      # Initialize the entry section if necessary
194                      if map['validation'][parent][entry_section] == nil
195                        map['validation'][parent][entry_section] = []
196                      end
197
198                      # Check for duplicate entries
199                      if map['validation'][parent][entry_section].include? entry
200                        error_found = true
201                        puts "VU Extraction Treeprocessor: ERROR - Valid Usage statement '#{entry}' is duplicated in the specification with VUID '#{vuid}'."
202                      end
203
204                      # Add the entry
205                      map['validation'][parent][entry_section] << entry
206
207                    else
208                      puts "VU Extraction Treeprocessor: WARNING - Valid Usage statement without a VUID found: "
209                      puts item_text
210                    end
211                  end
212                end
213              end
214            end
215          end
216
217        end
218      end
219    end
220
221    # Print out a list of VUIDs that were not extracted
222    if detected_vuid_list.length != 0
223      error_found = true
224      puts 'VU Extraction Treeprocessor: ERROR - Extraction failure'
225      puts
226      puts 'Some VUIDs were not successfully extracted from the specification.'
227      puts 'This is usually down to them appearing outside of a refpage (open)'
228      puts 'block; try checking where they are included.'
229      puts 'The following VUIDs were not extracted:'
230      detected_vuid_list.each do |vuid|
231        puts "\t * " + vuid
232      end
233    end
234
235    # Generate the json
236    json = JSON.pretty_generate(map)
237    outfile = document.attr('json_output')
238
239    # Verify the json against the schema, if the required gem is installed
240    begin
241      require 'json-schema'
242
243      # Read the schema in and validate against it
244      schema = IO.read(File.join(File.dirname(__FILE__), 'vu_schema.json'))
245      errors = JSON::Validator.fully_validate(schema, json, :errors_as_objects => true)
246
247      # Output errors if there were any
248      if errors != []
249        error_found = true
250        puts 'VU Extraction JSON Validator: ERROR - Validation of the json schema failed'
251        puts
252        puts 'It is likely that there is an invalid or malformed entry in the specification text,'
253        puts 'see below error messages for details, and use their VUIDs and text to correlate them to their location in the specification.'
254        puts
255
256        errors.each do |error|
257          puts error.to_s
258        end
259      end
260    rescue LoadError
261      puts 'VU Extraction JSON Validator: WARNING - "json-schema" gem missing - skipping verification of json output'
262      # error handling code here
263    end
264
265    # Write the file and exit - no further processing required.
266    IO.write(outfile, json)
267
268    if (error_found)
269      exit! 1
270    end
271    exit! 0
272  end
273end
274end
275